SUPPORTED_ENTITY_TYPES = EnumSet.of(
- EntityType.CUSTOMER,
- EntityType.USER,
- EntityType.ASSET,
- EntityType.DEVICE
- );
- private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
- @Mock
- private TbContext ctxMock;
- @Mock
- private UserService userServiceMock;
- @Mock
- private AssetService assetServiceMock;
- @Mock
- private DeviceService deviceServiceMock;
-
- @Test
- public void givenCustomerEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
- // GIVEN
- var customer = new Customer(new CustomerId(UUID.randomUUID()));
-
- // WHEN
- var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, customer.getId()).get();
-
- // THEN
- assertEquals(customer.getId(), actualCustomerId);
- }
-
- @Test
- public void givenUserEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
- // GIVEN
- var user = new User(new UserId(UUID.randomUUID()));
- var expectedCustomerId = new CustomerId(UUID.randomUUID());
- user.setCustomerId(expectedCustomerId);
-
- when(ctxMock.getUserService()).thenReturn(userServiceMock);
- doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(any(), any());
- when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
-
- // WHEN
- var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, user.getId()).get();
-
- // THEN
- assertEquals(expectedCustomerId, actualCustomerId);
- }
-
- @Test
- public void givenAssetEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
- // GIVEN
- var asset = new Asset(new AssetId(UUID.randomUUID()));
- var expectedCustomerId = new CustomerId(UUID.randomUUID());
- asset.setCustomerId(expectedCustomerId);
-
- when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
- doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(any(), any());
- when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
-
- // WHEN
- var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, asset.getId()).get();
-
- // THEN
- assertEquals(expectedCustomerId, actualCustomerId);
- }
-
- @Test
- public void givenDeviceEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
- // GIVEN
- var device = new Device(new DeviceId(UUID.randomUUID()));
- var expectedCustomerId = new CustomerId(UUID.randomUUID());
- device.setCustomerId(expectedCustomerId);
-
- when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
- doReturn(device).when(deviceServiceMock).findDeviceById(any(), any());
- when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
-
- // WHEN
- var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, device.getId()).get();
-
- // THEN
- assertEquals(expectedCustomerId, actualCustomerId);
- }
-
- @Test
- public void givenUnsupportedEntityTypes_whenFindEntityIdAsync_thenException() {
- for (var entityType : EntityType.values()) {
- if (!SUPPORTED_ENTITY_TYPES.contains(entityType)) {
- var entityId = EntityIdFactory.getByTypeAndUuid(entityType, UUID.randomUUID());
-
- var expectedExceptionMsg = "org.thingsboard.rule.engine.api.TbNodeException: Unexpected originator EntityType: " + entityType;
-
- var exception = assertThrows(ExecutionException.class,
- () -> EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, entityId).get());
-
- assertInstanceOf(TbNodeException.class, exception.getCause());
- assertEquals(expectedExceptionMsg, exception.getMessage());
- }
- }
- }
-
-}
diff --git a/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch
new file mode 100644
index 0000000000..c8090076cc
--- /dev/null
+++ b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch
@@ -0,0 +1,97 @@
+diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs
+index bf5480b..02c8246 100644
+--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs
++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs
+@@ -29,10 +29,10 @@ export class HexComponent {
+ }
+ }
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
+- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
+ }
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, decorators: [{
+ type: Component,
+- args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
++ args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
+ }] });
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGV4LWlucHV0LmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9oZXgtaW5wdXQvaGV4LWlucHV0LmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9oZXgtaW5wdXQvaGV4LWlucHV0LmNvbXBvbmVudC5odG1sIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxTQUFTLEVBQUUsdUJBQXVCLEVBQUUsZ0JBQWdCLEVBQWUsS0FBSyxFQUFFLEtBQUssRUFBZSxNQUFNLGVBQWUsQ0FBQztBQUM3SCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0saUNBQWlDLENBQUM7O0FBY3hELE1BQU0sT0FBTyxZQUFZO0lBWHpCO1FBYVcsVUFBSyxHQUF1QixLQUFLLENBQUMsUUFBUSxFQUFTLENBQUM7UUFFcEQsaUJBQVksR0FBeUIsS0FBSyxDQUFtQixLQUFLLEVBQUUsRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxnQkFBZ0IsRUFBRSxDQUFDLENBQUM7UUFFckgsZ0JBQVcsR0FBd0IsS0FBSyxDQUFTLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFDO0tBMkJwRjtJQXpCRyxJQUFXLEtBQUs7UUFDWixPQUFPLElBQUksQ0FBQyxXQUFXLEVBQUUsR0FBRyxDQUFDLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsS0FBSyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ2xJLENBQUM7SUFFTSxhQUFhLENBQUMsS0FBb0IsRUFBRSxVQUFrQjtRQUN6RCxNQUFNLEtBQUssR0FBRyxVQUFVLENBQUMsV0FBVyxFQUFFLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUV4RCxJQUNBLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxLQUFLLEVBQUUsSUFBSSxLQUFLLENBQUMsR0FBRyxDQUFDLFdBQVcsRUFBRSxLQUFLLE9BQU8sQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxDQUFDO2VBQ2xGLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFLENBQUM7WUFDMUMsTUFBTSxHQUFHLEdBQUcsUUFBUSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztZQUNoQyxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBRWhDOzs7OztlQUtHO1lBQ0gsSUFBSSxNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxNQUFNLEVBQUUsR0FBRyxDQUFDLEtBQUssS0FBSyxJQUFJLElBQUksQ0FBQyxLQUFLLEtBQUssS0FBSyxFQUFFLENBQUM7Z0JBQ3ZFLE1BQU0sUUFBUSxHQUFHLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxFQUFFLENBQUMsQ0FBQztnQkFDeEMsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7WUFDN0IsQ0FBQztRQUNMLENBQUM7SUFDTCxDQUFDOzhHQWhDUSxZQUFZO2tHQUFaLFlBQVksZ2dCQ2Z6Qiw0TUFLTTs7MkZEVU8sWUFBWTtrQkFYeEIsU0FBUzsrQkFDSSxxQkFBcUIsbUJBT2QsdUJBQXVCLENBQUMsTUFBTSxjQUNuQyxJQUFJIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgQ29tcG9uZW50LCBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneSwgYm9vbGVhbkF0dHJpYnV0ZSwgSW5wdXRTaWduYWwsIGlucHV0LCBtb2RlbCwgTW9kZWxTaWduYWwgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcclxuaW1wb3J0IHsgQ29sb3IgfSBmcm9tICcuLi8uLi8uLi8uLi9oZWxwZXJzL2NvbG9yLmNsYXNzJztcclxuXHJcblxyXG5AQ29tcG9uZW50KHtcclxuICAgIHNlbGVjdG9yOiBgaGV4LWlucHV0LWNvbXBvbmVudGAsXHJcbiAgICB0ZW1wbGF0ZVVybDogYC4vaGV4LWlucHV0LmNvbXBvbmVudC5odG1sYCxcclxuICAgIHN0eWxlVXJsczogW1xyXG4gICAgICAgIGAuLy4uLy4uL2Jhc2Uuc3R5bGUuc2Nzc2AsXHJcbiAgICAgICAgYC4vLi4vaW5wdXQuY29tcG9uZW50LnNjc3NgLFxyXG4gICAgICAgIGAuL2hleC1pbnB1dC5jb21wb25lbnQuc2Nzc2BcclxuICAgIF0sXHJcbiAgICBjaGFuZ2VEZXRlY3Rpb246IENoYW5nZURldGVjdGlvblN0cmF0ZWd5Lk9uUHVzaCxcclxuICAgIHN0YW5kYWxvbmU6IHRydWVcclxufSlcclxuZXhwb3J0IGNsYXNzIEhleENvbXBvbmVudCB7XHJcblxyXG4gICAgcHVibGljIGNvbG9yOiBNb2RlbFNpZ25hbDxDb2xvcj4gPSBtb2RlbC5yZXF1aXJlZDxDb2xvcj4oKTtcclxuXHJcbiAgICBwdWJsaWMgbGFiZWxWaXNpYmxlOiBJbnB1dFNpZ25hbDxib29sZWFuPiA9IGlucHV0PGJvb2xlYW4sIGJvb2xlYW4+KGZhbHNlLCB7IGFsaWFzOiAnbGFiZWwnLCB0cmFuc2Zvcm06IGJvb2xlYW5BdHRyaWJ1dGUgfSk7XHJcblxyXG4gICAgcHVibGljIHByZWZpeFZhbHVlOiBJbnB1dFNpZ25hbDxzdHJpbmc+ID0gaW5wdXQ8c3RyaW5nPignJywgeyBhbGlhczogJ3ByZWZpeCcgfSk7XHJcblxyXG4gICAgcHVibGljIGdldCB2YWx1ZSgpIHtcclxuICAgICAgICByZXR1cm4gdGhpcy5wcmVmaXhWYWx1ZSgpICsgKHRoaXMuY29sb3IoKSA/IHRoaXMuY29sb3IoKS50b0hleFN0cmluZyh0aGlzLmNvbG9yKCkuZ2V0UmdiYSgpLmFscGhhIDwgMSkucmVwbGFjZSgnIycsICcnKSA6ICcnKTtcclxuICAgIH1cclxuXHJcbiAgICBwdWJsaWMgb25JbnB1dENoYW5nZShldmVudDogS2V5Ym9hcmRFdmVudCwgaW5wdXRWYWx1ZTogc3RyaW5nKTogdm9pZCB7XHJcbiAgICAgICAgY29uc3QgdmFsdWUgPSBpbnB1dFZhbHVlLnRvTG93ZXJDYXNlKCkucmVwbGFjZSgnIycsICcnKTtcclxuXHJcbiAgICAgICAgaWYgKFxyXG4gICAgICAgICgoZXZlbnQua2V5Q29kZSA9PT0gMTMgfHwgZXZlbnQua2V5LnRvTG93ZXJDYXNlKCkgPT09ICdlbnRlcicpICYmIHZhbHVlLmxlbmd0aCA9PT0gMylcclxuICAgICAgICB8fCB2YWx1ZS5sZW5ndGggPT09IDYgfHwgdmFsdWUubGVuZ3RoID09PSA4KSB7XHJcbiAgICAgICAgICAgIGNvbnN0IGhleCA9IHBhcnNlSW50KHZhbHVlLCAxNik7XHJcbiAgICAgICAgICAgIGNvbnN0IGhleFN0ciA9IGhleC50b1N0cmluZygxNik7XHJcblxyXG4gICAgICAgICAgICAvKipcclxuICAgICAgICAgICAgICogaWYgdmFsdWUgaXMgdmFsaWRcclxuICAgICAgICAgICAgICogY2hhbmdlIGNvbG9yIGVsc2UgZG8gbm90aGluZ1xyXG4gICAgICAgICAgICAgKiBhZnRlciBwYXJzaW5nIG51bWJlciBsZWFkaW5nIDAgaXMgcmVtb3ZlZCxcclxuICAgICAgICAgICAgICogY29tcGFyZSBsZW5ndGggYW5kIGFkZCBsZWFkaW5nIDAgYmVmb3JlIGNvbXBhcmluZyB0d28gdmFsdWVzXHJcbiAgICAgICAgICAgICAqL1xyXG4gICAgICAgICAgICBpZiAoaGV4U3RyLnBhZFN0YXJ0KHZhbHVlLmxlbmd0aCwgJzAnKSA9PT0gdmFsdWUgJiYgdGhpcy52YWx1ZSAhPT0gdmFsdWUpIHtcclxuICAgICAgICAgICAgICAgIGNvbnN0IG5ld0NvbG9yID0gbmV3IENvbG9yKGAjJHt2YWx1ZX1gKTtcclxuICAgICAgICAgICAgICAgIHRoaXMuY29sb3Iuc2V0KG5ld0NvbG9yKTtcclxuICAgICAgICAgICAgfVxyXG4gICAgICAgIH1cclxuICAgIH1cclxufVxyXG4iLCI8ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICA8aW5wdXQgI2VsUmVmIHR5cGU9XCJ0ZXh0XCIgW3ZhbHVlXT1cInZhbHVlXCIgKGtleXVwKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCBlbFJlZi52YWx1ZSlcIiAvPlxyXG4gICAgQGlmIChsYWJlbFZpc2libGUoKSkge1xyXG4gICAgICAgIDxzcGFuPkhFWDwvc3Bhbj5cclxuICAgIH1cclxuPC9kaXY+Il19
+diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs
+index 2454b27..dd007ce 100644
+--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs
++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs
+@@ -21,10 +21,10 @@ export class HslaComponent {
+ this.color.set(newColor);
+ }
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
+- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
+ }
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, decorators: [{
+ type: Component,
+- args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
++ args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
+ }] });
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHNsYS1pbnB1dC5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9wcm9qZWN0cy9pcGxhYi9uZ3gtY29sb3ItcGlja2VyL3NyYy9saWIvY29tcG9uZW50cy9wYXJ0cy9pbnB1dHMvaHNsYS1pbnB1dC9oc2xhLWlucHV0LmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9oc2xhLWlucHV0L2hzbGEtaW5wdXQuY29tcG9uZW50Lmh0bWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSx1QkFBdUIsRUFBRSxnQkFBZ0IsRUFBZSxLQUFLLEVBQUUsS0FBSyxFQUFlLE1BQU0sZUFBZSxDQUFDO0FBQzdILE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxtQ0FBbUMsQ0FBQztBQUMxRCxPQUFPLEVBQUUseUJBQXlCLEVBQUUsTUFBTSxxREFBcUQsQ0FBQzs7QUFlaEcsTUFBTSxPQUFPLGFBQWE7SUFaMUI7UUFjVyxVQUFLLEdBQXVCLEtBQUssQ0FBQyxRQUFRLEVBQVMsQ0FBQztRQUVwRCxpQkFBWSxHQUF5QixLQUFLLENBQW1CLEtBQUssRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztRQUVySCxtQkFBYyxHQUF5QixLQUFLLENBQW1CLElBQUksRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztLQWdCaEk7SUFkRyxJQUFXLEtBQUs7UUFDWixPQUFPLElBQUksQ0FBQyxLQUFLLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQztJQUNuQyxDQUFDO0lBRU0sYUFBYSxDQUFDLFFBQWdCLEVBQUUsS0FBNEI7UUFDL0QsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQztRQUN6QixNQUFNLEdBQUcsR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUM7UUFDakQsTUFBTSxVQUFVLEdBQUcsS0FBSyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsVUFBVSxDQUFDO1FBQy9ELE1BQU0sU0FBUyxHQUFHLEtBQUssS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQztRQUM3RCxNQUFNLEtBQUssR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUM7UUFFckQsTUFBTSxRQUFRLEdBQUcsSUFBSSxLQUFLLEVBQUUsQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDeEUsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDN0IsQ0FBQzs4R0FyQlEsYUFBYTtrR0FBYixhQUFhLHNnQkNqQjFCLGlsQ0F5QkMseWVEVmEseUJBQXlCOzsyRkFFMUIsYUFBYTtrQkFaekIsU0FBUzsrQkFDSSxzQkFBc0IsbUJBT2YsdUJBQXVCLENBQUMsTUFBTSxjQUNuQyxJQUFJLFdBQ1AsQ0FBQyx5QkFBeUIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IENvbXBvbmVudCwgQ2hhbmdlRGV0ZWN0aW9uU3RyYXRlZ3ksIGJvb2xlYW5BdHRyaWJ1dGUsIElucHV0U2lnbmFsLCBpbnB1dCwgbW9kZWwsIE1vZGVsU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XHJcbmltcG9ydCB7IENvbG9yIH0gZnJvbSAnLi8uLi8uLi8uLi8uLi9oZWxwZXJzL2NvbG9yLmNsYXNzJztcclxuaW1wb3J0IHsgQ29sb3JQaWNrZXJJbnB1dERpcmVjdGl2ZSB9IGZyb20gJy4uLy4uLy4uLy4uL2RpcmVjdGl2ZXMvY29sb3ItcGlja2VyLWlucHV0LmRpcmVjdGl2ZSc7XHJcblxyXG5cclxuQENvbXBvbmVudCh7XHJcbiAgICBzZWxlY3RvcjogYGhzbGEtaW5wdXQtY29tcG9uZW50YCxcclxuICAgIHRlbXBsYXRlVXJsOiBgLi9oc2xhLWlucHV0LmNvbXBvbmVudC5odG1sYCxcclxuICAgIHN0eWxlVXJsczogW1xyXG4gICAgICAgIGAuLy4uLy4uL2Jhc2Uuc3R5bGUuc2Nzc2AsXHJcbiAgICAgICAgYC4vLi4vaW5wdXQuY29tcG9uZW50LnNjc3NgLFxyXG4gICAgICAgIGAuL2hzbGEtaW5wdXQuY29tcG9uZW50LnNjc3NgXHJcbiAgICBdLFxyXG4gICAgY2hhbmdlRGV0ZWN0aW9uOiBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneS5PblB1c2gsXHJcbiAgICBzdGFuZGFsb25lOiB0cnVlLFxyXG4gICAgaW1wb3J0czogW0NvbG9yUGlja2VySW5wdXREaXJlY3RpdmVdXHJcbn0pXHJcbmV4cG9ydCBjbGFzcyBIc2xhQ29tcG9uZW50IHtcclxuXHJcbiAgICBwdWJsaWMgY29sb3I6IE1vZGVsU2lnbmFsPENvbG9yPiA9IG1vZGVsLnJlcXVpcmVkPENvbG9yPigpO1xyXG5cclxuICAgIHB1YmxpYyBsYWJlbFZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4oZmFsc2UsIHsgYWxpYXM6ICdsYWJlbCcsIHRyYW5zZm9ybTogYm9vbGVhbkF0dHJpYnV0ZSB9KTtcclxuXHJcbiAgICBwdWJsaWMgaXNBbHBoYVZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4odHJ1ZSwgeyBhbGlhczogJ2FscGhhJywgdHJhbnNmb3JtOiBib29sZWFuQXR0cmlidXRlIH0pO1xyXG5cclxuICAgIHB1YmxpYyBnZXQgdmFsdWUoKSB7XHJcbiAgICAgICAgcmV0dXJuIHRoaXMuY29sb3IoKT8uZ2V0SHNsYSgpO1xyXG4gICAgfVxyXG5cclxuICAgIHB1YmxpYyBvbklucHV0Q2hhbmdlKG5ld1ZhbHVlOiBudW1iZXIsIGNvbG9yOiAnSCcgfCAnUycgfCAnTCcgfCAnQScpIHtcclxuICAgICAgICBjb25zdCB2YWx1ZSA9IHRoaXMudmFsdWU7XHJcbiAgICAgICAgY29uc3QgaHVlID0gY29sb3IgPT09ICdIJyA/IG5ld1ZhbHVlIDogdmFsdWUuaHVlO1xyXG4gICAgICAgIGNvbnN0IHNhdHVyYXRpb24gPSBjb2xvciA9PT0gJ1MnID8gbmV3VmFsdWUgOiB2YWx1ZS5zYXR1cmF0aW9uO1xyXG4gICAgICAgIGNvbnN0IGxpZ2h0bmVzcyA9IGNvbG9yID09PSAnTCcgPyBuZXdWYWx1ZSA6IHZhbHVlLmxpZ2h0bmVzcztcclxuICAgICAgICBjb25zdCBhbHBoYSA9IGNvbG9yID09PSAnQScgPyBuZXdWYWx1ZSA6IHZhbHVlLmFscGhhO1xyXG5cclxuICAgICAgICBjb25zdCBuZXdDb2xvciA9IG5ldyBDb2xvcigpLnNldEhzbGEoaHVlLCBzYXR1cmF0aW9uLCBsaWdodG5lc3MsIGFscGhhKTtcclxuICAgICAgICB0aGlzLmNvbG9yLnNldChuZXdDb2xvcik7XHJcbiAgICB9XHJcbn1cclxuIiwiPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIzNjBcIiBbdmFsdWVdPVwidmFsdWU/LmdldEh1ZSgpLnRvU3RyaW5nKClcIiAoaW5wdXRDaGFuZ2UpPVwib25JbnB1dENoYW5nZSgkZXZlbnQsICdIJylcIiAvPlxyXG4gICAgQGlmIChsYWJlbFZpc2libGUoKSkge1xyXG4gICAgICAgIDxzcGFuPkg8L3NwYW4+XHJcbiAgICB9XHJcbjwvZGl2PlxyXG48ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICA8aW5wdXQgdHlwZT1cInRleHRcIiBwYXR0ZXJuPVwiWzAtOV0qXCIgbWluPVwiMFwiIG1heD1cIjEwMFwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0U2F0dXJhdGlvbigpICsgJyUnXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnUycpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5TPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIxMDBcIiBbdmFsdWVdPVwidmFsdWU/LmdldExpZ2h0bmVzcygpICsgJyUnXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnTCcpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5MPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuQGlmIChpc0FscGhhVmlzaWJsZSgpKSB7XHJcbiAgICA8ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICAgICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKyhbXFwuLF1bMC05XXsxLDJ9KT9cIiBtaW49XCIwXCIgbWF4PVwiMVwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0QWxwaGEoKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnQScpXCIgLz5cclxuICAgICAgICBAaWYgKGxhYmVsVmlzaWJsZSgpKSB7XHJcbiAgICAgICAgICAgIDxzcGFuPkE8L3NwYW4+XHJcbiAgICAgICAgfVxyXG4gICAgPC9kaXY+XHJcbn0iXX0=
+diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs
+index 6d976e8..8989aad 100644
+--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs
++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs
+@@ -21,10 +21,10 @@ export class RgbaComponent {
+ this.color.set(newColor);
+ }
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
+- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
+ }
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, decorators: [{
+ type: Component,
+- args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
++ args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
+ }] });
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmdiYS1pbnB1dC5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9wcm9qZWN0cy9pcGxhYi9uZ3gtY29sb3ItcGlja2VyL3NyYy9saWIvY29tcG9uZW50cy9wYXJ0cy9pbnB1dHMvcmdiYS1pbnB1dC9yZ2JhLWlucHV0LmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9yZ2JhLWlucHV0L3JnYmEtaW5wdXQuY29tcG9uZW50Lmh0bWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSx1QkFBdUIsRUFBRSxnQkFBZ0IsRUFBZSxLQUFLLEVBQUUsS0FBSyxFQUFlLE1BQU0sZUFBZSxDQUFDO0FBQzdILE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxtQ0FBbUMsQ0FBQztBQUMxRCxPQUFPLEVBQUUseUJBQXlCLEVBQUUsTUFBTSxxREFBcUQsQ0FBQzs7QUFlaEcsTUFBTSxPQUFPLGFBQWE7SUFaMUI7UUFjVyxVQUFLLEdBQXVCLEtBQUssQ0FBQyxRQUFRLEVBQVMsQ0FBQztRQUVwRCxpQkFBWSxHQUF5QixLQUFLLENBQW1CLEtBQUssRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztRQUVySCxtQkFBYyxHQUF5QixLQUFLLENBQW1CLElBQUksRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztLQWdCaEk7SUFkRyxJQUFXLEtBQUs7UUFDWixPQUFPLElBQUksQ0FBQyxLQUFLLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQztJQUNuQyxDQUFDO0lBRU0sYUFBYSxDQUFDLFFBQWdCLEVBQUUsS0FBNEI7UUFDL0QsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQztRQUN6QixNQUFNLEdBQUcsR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUM7UUFDakQsTUFBTSxLQUFLLEdBQUcsS0FBSyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDO1FBQ3JELE1BQU0sSUFBSSxHQUFHLEtBQUssS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQztRQUNuRCxNQUFNLEtBQUssR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUM7UUFFckQsTUFBTSxRQUFRLEdBQUcsSUFBSSxLQUFLLEVBQUUsQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDOUQsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDN0IsQ0FBQzs4R0FyQlEsYUFBYTtrR0FBYixhQUFhLHNnQkNqQjFCLGlsQ0F5QkMseWVEVmEseUJBQXlCOzsyRkFFMUIsYUFBYTtrQkFaekIsU0FBUzsrQkFDSSxzQkFBc0IsbUJBT2YsdUJBQXVCLENBQUMsTUFBTSxjQUNuQyxJQUFJLFdBQ1AsQ0FBQyx5QkFBeUIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IENvbXBvbmVudCwgQ2hhbmdlRGV0ZWN0aW9uU3RyYXRlZ3ksIGJvb2xlYW5BdHRyaWJ1dGUsIElucHV0U2lnbmFsLCBpbnB1dCwgbW9kZWwsIE1vZGVsU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XHJcbmltcG9ydCB7IENvbG9yIH0gZnJvbSAnLi8uLi8uLi8uLi8uLi9oZWxwZXJzL2NvbG9yLmNsYXNzJztcclxuaW1wb3J0IHsgQ29sb3JQaWNrZXJJbnB1dERpcmVjdGl2ZSB9IGZyb20gJy4uLy4uLy4uLy4uL2RpcmVjdGl2ZXMvY29sb3ItcGlja2VyLWlucHV0LmRpcmVjdGl2ZSc7XHJcblxyXG5cclxuQENvbXBvbmVudCh7XHJcbiAgICBzZWxlY3RvcjogYHJnYmEtaW5wdXQtY29tcG9uZW50YCxcclxuICAgIHRlbXBsYXRlVXJsOiBgLi9yZ2JhLWlucHV0LmNvbXBvbmVudC5odG1sYCxcclxuICAgIHN0eWxlVXJsczogW1xyXG4gICAgICAgIGAuLy4uLy4uL2Jhc2Uuc3R5bGUuc2Nzc2AsXHJcbiAgICAgICAgYC4vLi4vaW5wdXQuY29tcG9uZW50LnNjc3NgLFxyXG4gICAgICAgIGAuL3JnYmEtaW5wdXQuY29tcG9uZW50LnNjc3NgXHJcbiAgICBdLFxyXG4gICAgY2hhbmdlRGV0ZWN0aW9uOiBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneS5PblB1c2gsXHJcbiAgICBzdGFuZGFsb25lOiB0cnVlLFxyXG4gICAgaW1wb3J0czogW0NvbG9yUGlja2VySW5wdXREaXJlY3RpdmVdXHJcbn0pXHJcbmV4cG9ydCBjbGFzcyBSZ2JhQ29tcG9uZW50IHtcclxuXHJcbiAgICBwdWJsaWMgY29sb3I6IE1vZGVsU2lnbmFsPENvbG9yPiA9IG1vZGVsLnJlcXVpcmVkPENvbG9yPigpO1xyXG5cclxuICAgIHB1YmxpYyBsYWJlbFZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4oZmFsc2UsIHsgYWxpYXM6ICdsYWJlbCcsIHRyYW5zZm9ybTogYm9vbGVhbkF0dHJpYnV0ZSB9KTtcclxuXHJcbiAgICBwdWJsaWMgaXNBbHBoYVZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4odHJ1ZSwgeyBhbGlhczogJ2FscGhhJywgdHJhbnNmb3JtOiBib29sZWFuQXR0cmlidXRlIH0pO1xyXG5cclxuICAgIHB1YmxpYyBnZXQgdmFsdWUoKSB7XHJcbiAgICAgICAgcmV0dXJuIHRoaXMuY29sb3IoKT8uZ2V0UmdiYSgpO1xyXG4gICAgfVxyXG5cclxuICAgIHB1YmxpYyBvbklucHV0Q2hhbmdlKG5ld1ZhbHVlOiBudW1iZXIsIGNvbG9yOiAnUicgfCAnRycgfCAnQicgfCAnQScpIHtcclxuICAgICAgICBjb25zdCB2YWx1ZSA9IHRoaXMudmFsdWU7XHJcbiAgICAgICAgY29uc3QgcmVkID0gY29sb3IgPT09ICdSJyA/IG5ld1ZhbHVlIDogdmFsdWUucmVkO1xyXG4gICAgICAgIGNvbnN0IGdyZWVuID0gY29sb3IgPT09ICdHJyA/IG5ld1ZhbHVlIDogdmFsdWUuZ3JlZW47XHJcbiAgICAgICAgY29uc3QgYmx1ZSA9IGNvbG9yID09PSAnQicgPyBuZXdWYWx1ZSA6IHZhbHVlLmJsdWU7XHJcbiAgICAgICAgY29uc3QgYWxwaGEgPSBjb2xvciA9PT0gJ0EnID8gbmV3VmFsdWUgOiB2YWx1ZS5hbHBoYTtcclxuXHJcbiAgICAgICAgY29uc3QgbmV3Q29sb3IgPSBuZXcgQ29sb3IoKS5zZXRSZ2JhKHJlZCwgZ3JlZW4sIGJsdWUsIGFscGhhKTtcclxuICAgICAgICB0aGlzLmNvbG9yLnNldChuZXdDb2xvcik7XHJcbiAgICB9XHJcbn1cclxuIiwiPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIyNTVcIiBbdmFsdWVdPVwidmFsdWU/LmdldFJlZCgpLnRvU3RyaW5nKClcIiAoaW5wdXRDaGFuZ2UpPVwib25JbnB1dENoYW5nZSgkZXZlbnQsICdSJylcIiAvPlxyXG4gICAgQGlmIChsYWJlbFZpc2libGUoKSkge1xyXG4gICAgICAgIDxzcGFuPlI8L3NwYW4+XHJcbiAgICB9XHJcbjwvZGl2PlxyXG48ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICA8aW5wdXQgdHlwZT1cInRleHRcIiBwYXR0ZXJuPVwiWzAtOV0qXCIgbWluPVwiMFwiIG1heD1cIjI1NVwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0R3JlZW4oKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnRycpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5HPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIyNTVcIiBbdmFsdWVdPVwidmFsdWU/LmdldEJsdWUoKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnQicpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5CPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuQGlmIChpc0FscGhhVmlzaWJsZSgpKSB7XHJcbiAgICA8ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICAgICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKyhbXFwuLF1bMC05XXsxLDJ9KT9cIiBtaW49XCIwXCIgbWF4PVwiMVwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0QWxwaGEoKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnQScpXCIgLz5cclxuICAgICAgICBAaWYgKGxhYmVsVmlzaWJsZSgpKSB7XHJcbiAgICAgICAgICAgIDxzcGFuPkE8L3NwYW4+XHJcbiAgICAgICAgfVxyXG4gICAgPC9kaXY+XHJcbn0iXX0=
+diff --git a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs
+index a3b270c..40bea7f 100644
+--- a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs
++++ b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs
+@@ -1123,11 +1123,11 @@ class RgbaComponent {
+ this.color.set(newColor);
+ }
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
+- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
+ }
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, decorators: [{
+ type: Component,
+- args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
++ args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
+ }] });
+
+ class HslaComponent {
+@@ -1149,11 +1149,11 @@ class HslaComponent {
+ this.color.set(newColor);
+ }
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
+- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
+ }
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, decorators: [{
+ type: Component,
+- args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
++ args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
+ }] });
+
+ class HexComponent {
+@@ -1184,11 +1184,11 @@ class HexComponent {
+ }
+ }
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
+- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
+ }
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, decorators: [{
+ type: Component,
+- args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
++ args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }]
+ }] });
+
+ const OpacityAnimation = trigger('opacityAnimation', [
diff --git a/ui-ngx/src/app/core/guards/auth.guard.ts b/ui-ngx/src/app/core/guards/auth.guard.ts
index f8bbcc3241..5090af97ec 100644
--- a/ui-ngx/src/app/core/guards/auth.guard.ts
+++ b/ui-ngx/src/app/core/guards/auth.guard.ts
@@ -131,7 +131,7 @@ export class AuthGuard {
}
}
if (this.mobileService.isMobileApp() && !path.startsWith('dashboard.')) {
- this.mobileService.handleMobileNavigation(path, params);
+ this.mobileService.handleMobileNavigation(path, params, lastChild.queryParams);
return of(false);
}
if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) {
diff --git a/ui-ngx/src/app/core/http/admin.service.ts b/ui-ngx/src/app/core/http/admin.service.ts
index 55eaa16b90..36bb931f93 100644
--- a/ui-ngx/src/app/core/http/admin.service.ts
+++ b/ui-ngx/src/app/core/http/admin.service.ts
@@ -21,9 +21,9 @@ import { HttpClient } from '@angular/common/http';
import {
AdminSettings,
AutoCommitSettings,
- MailConfigTemplate,
FeaturesInfo,
JwtSettings,
+ MailConfigTemplate,
MailServerSettings,
RepositorySettings,
RepositorySettingsInfo,
diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts
index 66c0cb609e..631550ae9e 100644
--- a/ui-ngx/src/app/core/http/calculated-fields.service.ts
+++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts
@@ -15,11 +15,15 @@
///
import { Injectable } from '@angular/core';
-import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
+import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data';
-import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/models/calculated-field.models';
+import {
+ CalculatedField,
+ CalculatedFieldTestScriptInputParams,
+ CalculatedFieldType
+} from '@shared/models/calculated-field.models';
import { PageLink } from '@shared/models/page/page-link';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityTestScriptResult } from '@shared/models/entity.models';
@@ -46,9 +50,8 @@ export class CalculatedFieldsService {
return this.http.delete(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config));
}
- public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> {
- return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`,
- defaultHttpOptionsFromConfig(config));
+ public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable> {
+ return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, createDefaultHttpOptions(type ? {type} : null, config));
}
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable {
diff --git a/ui-ngx/src/app/core/http/git-hub.service.ts b/ui-ngx/src/app/core/http/git-hub.service.ts
new file mode 100644
index 0000000000..b6a927dab3
--- /dev/null
+++ b/ui-ngx/src/app/core/http/git-hub.service.ts
@@ -0,0 +1,38 @@
+///
+/// Copyright © 2016-2025 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Injectable } from '@angular/core';
+import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
+import { Observable, of } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
+import { catchError, map } from 'rxjs/operators';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class GitHubService {
+
+ constructor(
+ private http: HttpClient
+ ) { }
+
+ public getGitHubStar(config?: RequestConfig): Observable {
+ return this.http.get('https://api.github.com/repos/thingsboard/thingsboard', defaultHttpOptionsFromConfig(config)).pipe(
+ catchError(() => of({})),
+ map((res: any) => res?.stargazers_count ?? 0)
+ )
+ }
+}
diff --git a/ui-ngx/src/app/core/services/mobile.service.ts b/ui-ngx/src/app/core/services/mobile.service.ts
index 6fcb7c3602..aceb564ec2 100644
--- a/ui-ngx/src/app/core/services/mobile.service.ts
+++ b/ui-ngx/src/app/core/services/mobile.service.ts
@@ -106,9 +106,9 @@ export class MobileService {
}
}
- public handleMobileNavigation(path?: string, params?: Params) {
+ public handleMobileNavigation(path?: string, params?: Params, queryParams?: Params) {
if (this.mobileApp) {
- this.mobileChannel.callHandler(navigationHandler, path, params);
+ this.mobileChannel.callHandler(navigationHandler, path, params, queryParams);
}
}
diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts
index 82382a08eb..c68f26d56f 100644
--- a/ui-ngx/src/app/core/utils.ts
+++ b/ui-ngx/src/app/core/utils.ts
@@ -34,8 +34,10 @@ import {
} from '@shared/models/js-function.models';
import { DomSanitizer } from '@angular/platform-browser';
import { SecurityContext } from '@angular/core';
+import { AbstractControl, ValidationErrors, Validators } from '@angular/forms';
const varsRegex = /\${([^}]*)}/g;
+const emailRegex = /^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
export function onParentScrollOrWindowResize(el: Node): Observable {
const scrollSubject = new Subject();
@@ -1033,3 +1035,11 @@ export const trimDefaultValues = (input: Record, defaults: Record {
+ if (isUndefinedOrNull(control.value) || (typeof control.value === 'string' && control.value.length === 0)) {
+ return null;
+ }
+ return emailRegex.test(control.value) ? null : {email: true};
+};
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html
new file mode 100644
index 0000000000..9e5036f6ff
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html
@@ -0,0 +1,49 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts
new file mode 100644
index 0000000000..8a55f52678
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts
@@ -0,0 +1,60 @@
+///
+/// Copyright © 2016-2025 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, Inject } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { FormBuilder, FormControl } from '@angular/forms';
+import { Router } from '@angular/router';
+import { DialogComponent } from '@app/shared/components/dialog.component';
+
+export interface AlarmRuleDetailsDialogData {
+ alarmDetails: string;
+ readonly: boolean;
+}
+
+@Component({
+ selector: 'tb-edit-alarm-details-dialog',
+ templateUrl: './alarm-rule-details-dialog.component.html',
+ providers: [],
+ styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
+})
+export class AlarmRuleDetailsDialogComponent extends DialogComponent {
+
+ alarmDetailsControl: FormControl;
+
+ constructor(protected store: Store,
+ protected router: Router,
+ @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDetailsDialogData,
+ public dialogRef: MatDialogRef,
+ private fb: FormBuilder) {
+ super(store, router, dialogRef);
+
+ this.alarmDetailsControl = this.fb.control(this.data.alarmDetails);
+ if (this.data.readonly) {
+ this.alarmDetailsControl.disable();
+ }
+ }
+
+ cancel(): void {
+ this.dialogRef.close(null);
+ }
+
+ save(): void {
+ this.dialogRef.close(this.alarmDetailsControl.value);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
new file mode 100644
index 0000000000..04423edea6
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
@@ -0,0 +1,164 @@
+
+
+
+ {{ 'alarm-rule.alarm-rule' | translate}}
+
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss
new file mode 100644
index 0000000000..04e46fd3d0
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2025 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.
+ */
+
+.calculated-field-dialog-container {
+ width: 869px;
+ max-width: 100%;
+}
+
+.clear-alarm-rule {
+ border: 1px solid rgba(0, 0, 0, 0.12);
+ border-left-width: 4px;
+ border-left-color: green;
+ border-radius: 4px;
+ padding: 8px;
+ min-width: 0;
+}
+
+.button-icon {
+ color: rgba(0, 0, 0, 0.38);
+ min-width: 40px;
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
new file mode 100644
index 0000000000..0ea3bd036e
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
@@ -0,0 +1,179 @@
+///
+/// Copyright © 2016-2025 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, ViewEncapsulation } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+import { DialogComponent } from '@shared/components/dialog.component';
+import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models';
+import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
+import { EntityType } from '@shared/models/entity-type.models';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { ScriptLanguage } from '@shared/models/rule-node.models';
+import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
+import { EntityId } from '@shared/models/id/entity-id';
+import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
+import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes";
+import { MatChipInputEvent } from "@angular/material/chips";
+import { AlarmRule, AlarmRuleConditionType, AlarmRuleExpressionType } from "@shared/models/alarm-rule.models";
+import { deepTrim } from "@core/utils";
+
+export interface AlarmRuleDialogData {
+ value?: CalculatedField;
+ buttonTitle: string;
+ entityId: EntityId;
+ tenantId: string;
+ entityName?: string;
+ ownerId: EntityId;
+ additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
+ isDirty?: boolean;
+}
+
+@Component({
+ selector: 'tb-alarm-rule-dialog',
+ templateUrl: './alarm-rule-dialog.component.html',
+ styleUrls: ['./alarm-rule-dialog.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class AlarmRuleDialogComponent extends DialogComponent {
+
+ fieldFormGroup = this.fb.group({
+ name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
+ type: [CalculatedFieldType.ALARM],
+ debugSettings: [],
+ configuration: this.fb.group({
+ arguments: this.fb.control({}),
+ propagate: [false],
+ propagateToOwner: [false],
+ propagateToTenant: [false],
+ propagateRelationTypes: [null],
+ createRules: [null],
+ clearRule: [null],
+ }),
+ });
+
+ additionalDebugActionConfig = this.data.value?.id ? {
+ ...this.data.additionalDebugActionConfig,
+ action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }),
+ } : null;
+
+ readonly EntityType = EntityType;
+ readonly CalculatedFieldType = CalculatedFieldType;
+ readonly ScriptLanguage = ScriptLanguage;
+
+ separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
+
+ constructor(protected store: Store,
+ protected router: Router,
+ @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
+ protected dialogRef: MatDialogRef,
+ private calculatedFieldsService: CalculatedFieldsService,
+ private destroyRef: DestroyRef,
+ private fb: FormBuilder) {
+ super(store, router, dialogRef);
+ this.observeIsLoading();
+ this.applyDialogData();
+ }
+
+ get configFormGroup(): FormGroup {
+ return this.fieldFormGroup.get('configuration') as FormGroup;
+ }
+
+ get arguments(): Record {
+ return this.fieldFormGroup.get('configuration.arguments').value;
+ }
+
+ public removeClearAlarmRule() {
+ this.configFormGroup.patchValue({clearRule: null});
+ this.fieldFormGroup.markAsDirty();
+ }
+
+ public addClearAlarmRule() {
+ const clearAlarmRule: AlarmRule = {
+ condition: {
+ type: AlarmRuleConditionType.SIMPLE,
+ expression: {
+ type: AlarmRuleExpressionType.SIMPLE
+ }
+ }
+ };
+ this.configFormGroup.patchValue({clearRule: clearAlarmRule});
+ }
+
+ removeRelationType(key: string): void {
+ const keys: string[] = this.configFormGroup.get('propagateRelationTypes').value;
+ const index = keys.indexOf(key);
+ if (index >= 0) {
+ keys.splice(index, 1);
+ this.configFormGroup.get('propagateRelationTypes').setValue(keys);
+ }
+ }
+
+ addRelationType(event: MatChipInputEvent): void {
+ const input = event.chipInput.inputElement;
+ let value = (event.value ?? '').trim();
+ if (value) {
+ let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value ?? [];
+ if (keys.indexOf(value) === -1) {
+ keys.push(value);
+ this.configFormGroup.get('propagateRelationTypes').setValue(keys);
+ }
+ }
+ if (input) {
+ input.value = '';
+ }
+ }
+
+ get fromGroupValue(): CalculatedField {
+ return deepTrim(this.fieldFormGroup.value as CalculatedField);
+ }
+
+ cancel(): void {
+ this.dialogRef.close(null);
+ }
+
+ add(): void {
+ if (this.fieldFormGroup.valid) {
+ const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue};
+ alarmRule.configuration.type = CalculatedFieldType.ALARM;
+
+ this.calculatedFieldsService.saveCalculatedField(alarmRule)
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(calculatedField => this.dialogRef.close(calculatedField));
+ }
+ }
+
+ private applyDialogData(): void {
+ const { configuration = {}, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
+ this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false});
+ }
+
+ private observeIsLoading(): void {
+ this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => {
+ if (loading) {
+ this.fieldFormGroup.disable({emitEvent: false});
+ } else {
+ this.fieldFormGroup.enable({emitEvent: false});
+ if (this.data.isDirty) {
+ this.fieldFormGroup.markAsDirty();
+ }
+ }
+ });
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts
new file mode 100644
index 0000000000..7acfca7b0d
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts
@@ -0,0 +1,80 @@
+///
+/// Copyright © 2016-2025 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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SharedModule } from '@shared/shared.module';
+import { AlarmRuleDialogComponent } from "@home/components/alarm-rules/alarm-rule-dialog.component";
+import { CreateCfAlarmRulesComponent } from "@home/components/alarm-rules/create-cf-alarm-rules.component";
+import { CfAlarmRuleComponent } from "@home/components/alarm-rules/cf-alarm-rule.component";
+import { CfAlarmRuleConditionComponent } from "@home/components/alarm-rules/cf-alarm-rule-condition.component";
+import {
+ CfAlarmRuleConditionDialogComponent
+} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component";
+import { CfAlarmScheduleComponent } from "@home/components/alarm-rules/cf-alarm-schedule.component";
+import { CfAlarmScheduleDialogComponent } from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component";
+import {
+ EntityDebugSettingsButtonComponent
+} from "@home/components/entity/debug/entity-debug-settings-button.component";
+import { AlarmRuleFilterTextComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-text.component";
+import {
+ CalculatedFieldArgumentsTableModule
+} from "@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module";
+import {
+ AlarmRuleFilterPredicateListComponent
+} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component";
+import {
+ AlarmRuleFilterPredicateComponent
+} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate.component";
+import {
+ AlarmRuleFilterPredicateValueComponent
+} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component";
+import {
+ AlarmRuleComplexFilterPredicateDialogComponent
+} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component";
+import { AlarmRuleFilterListComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-list.component";
+import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component";
+import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
+
+@NgModule({
+ declarations: [
+ AlarmRuleDialogComponent,
+ CreateCfAlarmRulesComponent,
+ CfAlarmRuleComponent,
+ CfAlarmRuleConditionComponent,
+ CfAlarmRuleConditionDialogComponent,
+ CfAlarmScheduleComponent,
+ CfAlarmScheduleDialogComponent,
+ AlarmRuleFilterTextComponent,
+ AlarmRuleFilterListComponent,
+ AlarmRuleFilterDialogComponent,
+ AlarmRuleFilterPredicateListComponent,
+ AlarmRuleFilterPredicateComponent,
+ AlarmRuleFilterPredicateValueComponent,
+ AlarmRuleComplexFilterPredicateDialogComponent,
+ AlarmRuleDetailsDialogComponent,
+ ],
+ imports: [
+ CommonModule,
+ SharedModule,
+ EntityDebugSettingsButtonComponent,
+ CalculatedFieldArgumentsTableModule
+ ],
+ exports: [
+ AlarmRuleDialogComponent,
+ ]
+})
+export class AlarmRuleModule { }
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
new file mode 100644
index 0000000000..fb31b8b522
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
@@ -0,0 +1,278 @@
+///
+/// Copyright © 2016-2025 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 {
+ checkBoxCell,
+ DateEntityTableColumn,
+ EntityTableColumn,
+ EntityTableConfig
+} from '@home/models/entity/entities-table-config.models';
+import { EntityType } from '@shared/models/entity-type.models';
+import { TranslateService } from '@ngx-translate/core';
+import { Direction } from '@shared/models/page/sort-order';
+import { MatDialog } from '@angular/material/dialog';
+import { PageLink } from '@shared/models/page/page-link';
+import { Observable, of } from 'rxjs';
+import { PageData } from '@shared/models/page/page-data';
+import { EntityId } from '@shared/models/id/entity-id';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { getCurrentAuthUser } from '@core/auth/auth.selectors';
+import { DestroyRef, Renderer2 } from '@angular/core';
+import { EntityDebugSettings } from '@shared/models/entity.models';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
+import { catchError, filter, switchMap } from 'rxjs/operators';
+import {
+ ArgumentEntityType,
+ CalculatedField,
+ CalculatedFieldAlarmRule,
+ CalculatedFieldType,
+} from '@shared/models/calculated-field.models';
+import { ImportExportService } from '@shared/import-export/import-export.service';
+import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
+import { DatePipe } from '@angular/common';
+import {
+ AlarmRuleDialogComponent,
+ AlarmRuleDialogData
+} from "@home/components/alarm-rules/alarm-rule-dialog.component";
+import {
+ CalculatedFieldDebugDialogComponent,
+ CalculatedFieldDebugDialogData
+} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component";
+import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models";
+
+export class AlarmRulesTableConfig extends EntityTableConfig {
+
+ readonly tenantId = getCurrentAuthUser(this.store).tenantId;
+ additionalDebugActionConfig = {
+ title: this.translate.instant('calculated-fields.see-debug-events'),
+ action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField),
+ };
+
+ constructor(private calculatedFieldsService: CalculatedFieldsService,
+ private translate: TranslateService,
+ private dialog: MatDialog,
+ private datePipe: DatePipe,
+ public entityId: EntityId = null,
+ private store: Store,
+ private destroyRef: DestroyRef,
+ private renderer: Renderer2,
+ public entityName: string,
+ private ownerId: EntityId = null,
+ private importExportService: ImportExportService,
+ private entityDebugSettingsService: EntityDebugSettingsService,
+ ) {
+ super();
+ this.tableTitle = this.translate.instant('alarm-rule.alarm-rules');
+ this.detailsPanelEnabled = false;
+ this.pageMode = false;
+ this.entityType = EntityType.CALCULATED_FIELD;
+ this.entityTranslations = {
+ type: 'alarm-rule.alarm-rule',
+ typePlural: 'alarm-rule.alarm-rules',
+ list: 'alarm-rule.list',
+ add: 'action.add',
+ noEntities: 'alarm-rule.no-found',
+ search: 'action.search',
+ selectedEntities: 'alarm-rule.selected-fields'
+ };
+
+ this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink);
+ this.addEntity = this.getCalculatedAlarmDialog.bind(this);
+ this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('alarm-rule.delete-title', {title: field.name});
+ this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text');
+ this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count});
+ this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text');
+ this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
+ this.addActionDescriptors = [
+ {
+ name: this.translate.instant('alarm-rule.create'),
+ icon: 'insert_drive_file',
+ isEnabled: () => true,
+ onAction: ($event) => this.getTable().addEntity($event)
+ },
+ {
+ name: this.translate.instant('alarm-rule.import'),
+ icon: 'file_upload',
+ isEnabled: () => true,
+ onAction: () => this.importCalculatedField()
+ }
+ ];
+
+ this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
+ this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'));
+ this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%'));
+ this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', '67%',
+ entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '),
+ () => ({}), false));
+ this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '60px',
+ entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: '0 14px'}}, false));
+
+ this.cellActionDescriptors.push(
+ {
+ name: this.translate.instant('action.export'),
+ icon: 'file_download',
+ isEnabled: () => true,
+ onAction: (event$, entity) => this.exportAlarmRule(event$, entity),
+ },
+ {
+ name: this.translate.instant('entity-view.events'),
+ icon: 'mdi:clipboard-text-clock',
+ isEnabled: () => true,
+ onAction: (_, entity) => this.openDebugEventsDialog(entity),
+ },
+ {
+ name: '',
+ nameFunction: entity => this.entityDebugSettingsService.getDebugConfigLabel(entity?.debugSettings),
+ icon: 'mdi:bug',
+ isEnabled: () => true,
+ iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline',
+ onAction: ($event, entity) => this.onOpenDebugConfig($event, entity),
+ },
+ {
+ name: this.translate.instant('action.edit'),
+ icon: 'edit',
+ isEnabled: () => true,
+ onAction: (_, entity) => this.editCalculatedField(entity),
+ }
+ );
+ }
+
+ fetchCalculatedFields(pageLink: PageLink): Observable> {
+ return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM);
+ }
+
+ onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void {
+ const { debugSettings = {}, id } = calculatedField;
+ const additionalActionConfig = {
+ ...this.additionalDebugActionConfig,
+ action: () => this.openDebugEventsDialog(calculatedField)
+ };
+ if ($event) {
+ $event.stopPropagation();
+ }
+
+ const { viewContainerRef, renderer } = this.entityDebugSettingsService;
+ if (!viewContainerRef || !renderer) {
+ this.entityDebugSettingsService.viewContainerRef = this.getTable().viewContainerRef;
+ this.entityDebugSettingsService.renderer = this.renderer;
+ }
+
+ this.entityDebugSettingsService.openDebugStrategyPanel({
+ debugSettings,
+ debugConfig: {
+ entityType: EntityType.CALCULATED_FIELD,
+ entityLabel: 'alarm-rule.alarm-rule',
+ additionalActionConfig,
+ },
+ onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings)
+ }, $event.target as Element);
+ }
+
+ private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
+ this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty)
+ .subscribe((res) => {
+ if (res) {
+ this.updateData();
+ }
+ });
+ }
+
+ private getCalculatedAlarmDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable {
+ return this.dialog.open(AlarmRuleDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ value,
+ buttonTitle,
+ entityId: this.entityId,
+ tenantId: this.tenantId,
+ entityName: this.entityName,
+ ownerId: this.ownerId,
+ additionalDebugActionConfig: this.additionalDebugActionConfig,
+ isDirty,
+ },
+ enterAnimationDuration: isDirty ? 0 : null,
+ })
+ .afterClosed()
+ .pipe(filter(Boolean));
+ }
+
+ private openDebugEventsDialog(calculatedField: CalculatedField): void {
+ this.dialog.open(CalculatedFieldDebugDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ tenantId: this.tenantId,
+ value: calculatedField,
+ getTestScriptDialogFn: null,
+ }
+ })
+ .afterClosed()
+ .subscribe();
+ }
+
+ private exportAlarmRule($event: Event, calculatedField: CalculatedField): void {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ this.importExportService.exportCalculatedField(calculatedField.id.id);
+ }
+
+ private importCalculatedField(): void {
+ this.importExportService.openCalculatedFieldImportDialog()
+ .pipe(
+ filter(Boolean),
+ switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)),
+ filter(Boolean),
+ switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
+ filter(Boolean),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe(() => this.updateData());
+ }
+
+ private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField {
+ if (calculatedField.type === CalculatedFieldType.GEOFENCING) {
+ calculatedField.configuration.zoneGroups = Object.keys(calculatedField.configuration.zoneGroups).reduce((acc, key) => {
+ const arg = calculatedField.configuration.zoneGroups[key];
+ acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
+ ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } }
+ : arg;
+ return acc;
+ }, {});
+ } else {
+ calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
+ const arg = calculatedField.configuration.arguments[key];
+ acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
+ ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } }
+ : arg;
+ return acc;
+ }, {});
+ }
+
+ return calculatedField;
+ }
+
+ private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void {
+ this.calculatedFieldsService.getCalculatedFieldById(id).pipe(
+ switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })),
+ catchError(() => of(null)),
+ takeUntilDestroyed(this.destroyRef),
+ ).subscribe(() => this.updateData());
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html
new file mode 100644
index 0000000000..e9eebb226c
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html
@@ -0,0 +1,20 @@
+
+@if (alarmRulesTableConfig) {
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss
new file mode 100644
index 0000000000..0e0dacc392
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016-2025 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-entities-table {
+ --mat-sidenav-content-background-color: white;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
new file mode 100644
index 0000000000..9a553c1ffa
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
@@ -0,0 +1,88 @@
+///
+/// Copyright © 2016-2025 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 {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ DestroyRef,
+ effect,
+ input,
+ Renderer2,
+ ViewChild,
+} from '@angular/core';
+import { EntityId } from '@shared/models/id/entity-id';
+import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
+import { TranslateService } from '@ngx-translate/core';
+import { MatDialog } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
+import { ImportExportService } from '@shared/import-export/import-export.service';
+import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
+import { DatePipe } from '@angular/common';
+import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config";
+
+@Component({
+ selector: 'tb-alarm-rules-table',
+ templateUrl: './alarm-rules-table.component.html',
+ styleUrls: ['./alarm-rules-table.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [EntityDebugSettingsService]
+})
+export class AlarmRulesTableComponent {
+
+ @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent;
+
+ active = input();
+ entityId = input();
+ entityName = input();
+ ownerId = input();
+
+ alarmRulesTableConfig: AlarmRulesTableConfig;
+
+ constructor(private calculatedFieldsService: CalculatedFieldsService,
+ private translate: TranslateService,
+ private dialog: MatDialog,
+ private store: Store,
+ private datePipe: DatePipe,
+ private cd: ChangeDetectorRef,
+ private renderer: Renderer2,
+ private importExportService: ImportExportService,
+ private entityDebugSettingsService: EntityDebugSettingsService,
+ private destroyRef: DestroyRef) {
+
+ effect(() => {
+ if (this.active()) {
+ this.alarmRulesTableConfig = new AlarmRulesTableConfig(
+ this.calculatedFieldsService,
+ this.translate,
+ this.dialog,
+ this.datePipe,
+ this.entityId(),
+ this.store,
+ this.destroyRef,
+ this.renderer,
+ this.entityName(),
+ this.ownerId(),
+ this.importExportService,
+ this.entityDebugSettingsService,
+ );
+ this.cd.markForCheck();
+ }
+ });
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
new file mode 100644
index 0000000000..d2f98ecd85
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
@@ -0,0 +1,188 @@
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
new file mode 100644
index 0000000000..48b290b32c
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
@@ -0,0 +1,226 @@
+///
+/// Copyright © 2016-2025 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, Inject } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { FormBuilder, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+import { DialogComponent } from '@app/shared/components/dialog.component';
+import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { ScriptLanguage } from "@shared/models/rule-node.models";
+import {
+ AlarmRuleCondition,
+ AlarmRuleConditionType,
+ AlarmRuleConditionTypeTranslationMap,
+ AlarmRuleExpressionType
+} from "@shared/models/alarm-rule.models";
+import {
+ CalculatedFieldArgument,
+ getCalculatedFieldArgumentsEditorCompleter,
+ getCalculatedFieldArgumentsHighlights
+} from "@shared/models/calculated-field.models";
+import { TbEditorCompleter } from "@shared/models/ace/completion.models";
+import { AceHighlightRules } from "@shared/models/ace/ace.models";
+import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models";
+
+export interface CfAlarmRuleConditionDialogData {
+ readonly: boolean;
+ condition: AlarmRuleCondition;
+ arguments?: Record;
+}
+
+@Component({
+ selector: 'tb-cf-alarm-rule-condition-dialog',
+ templateUrl: './cf-alarm-rule-condition-dialog.component.html',
+ providers: [],
+ styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
+})
+export class CfAlarmRuleConditionDialogComponent extends DialogComponent {
+
+ AlarmRuleExpressionType = AlarmRuleExpressionType;
+
+ timeUnits = Object.values(TimeUnit);
+ timeUnitTranslations = timeUnitTranslationMap;
+ alarmConditionTypes = Object.values(AlarmRuleConditionType);
+ AlarmConditionType = AlarmRuleConditionType;
+ alarmConditionTypeTranslation = AlarmRuleConditionTypeTranslationMap;
+ readonly = this.data.readonly;
+ condition = this.data.condition;
+
+ conditionFormGroup = this.fb.group({
+ expression: this.fb.group({
+ type: [AlarmRuleExpressionType.SIMPLE],
+ expression: ['', [Validators.required]],
+ operation: [ComplexOperation.AND],
+ filters: [null],
+ }),
+ type: this.fb.control(AlarmRuleConditionType.SIMPLE, Validators.required),
+ unit: this.fb.control(TimeUnit.SECONDS, Validators.required),
+ value: this.fb.group({
+ staticValue: this.fb.control(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]),
+ dynamicValueArgument: this.fb.control('', Validators.required),
+ }),
+ count: this.fb.group({
+ staticValue: this.fb.control(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]),
+ dynamicValueArgument: this.fb.control('', Validators.required),
+ }),
+ });
+
+ readonly scriptLanguage = ScriptLanguage;
+
+ defaultValuePlaceholder = '';
+ defaultValueRequiredError = '';
+ defaultValueRangeError = '';
+ defaultValuePatternError = '';
+
+ durationDynamicModeControl = this.fb.control(false);
+ repeatingDynamicModeControl = this.fb.control(false);
+
+ ComplexOperation = ComplexOperation;
+ complexOperationTranslationMap = complexOperationTranslationMap;
+
+ functionArgs: Array;
+ argumentsEditorCompleter: TbEditorCompleter;
+ argumentsHighlightRules: AceHighlightRules;
+
+ arguments = this.data.arguments;
+ argumentsList: Array;
+
+ constructor(protected store: Store,
+ protected router: Router,
+ @Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData,
+ public dialogRef: MatDialogRef,
+ private fb: FormBuilder) {
+ super(store, router, dialogRef);
+
+ this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)];
+ this.argumentsEditorCompleter = getCalculatedFieldArgumentsEditorCompleter(this.data.arguments);
+ this.argumentsHighlightRules = getCalculatedFieldArgumentsHighlights(this.data.arguments);
+ this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
+
+ this.conditionFormGroup.patchValue({
+ expression: {
+ type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE,
+ expression: this.condition?.expression?.expression ?? null,
+ filters: this.condition?.expression?.filters ?? [],
+ operation: this.condition?.expression?.operation ?? ComplexOperation.AND
+ },
+ type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE,
+ unit: this.condition?.unit ?? TimeUnit.SECONDS,
+ value: {
+ staticValue: this.condition?.value?.staticValue,
+ dynamicValueArgument: this.condition?.value?.dynamicValueArgument,
+ },
+ count: {
+ staticValue: this.condition?.count?.staticValue ?? null,
+ dynamicValueArgument: this.condition?.count?.dynamicValueArgument
+ }
+ }, {emitEvent: false});
+
+ this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false});
+ this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false});
+
+ this.conditionFormGroup.get('type').valueChanges.pipe(
+ takeUntilDestroyed()
+ ).subscribe((type) => {
+ this.updateValidators(type, true);
+ });
+
+ this.conditionFormGroup.get('expression.type').valueChanges.pipe(
+ takeUntilDestroyed()
+ ).subscribe((type) => {
+ this.updateExpressionTypeValidator(type);
+ this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
+ });
+
+ this.durationDynamicModeControl.valueChanges.pipe(
+ takeUntilDestroyed()
+ ).subscribe((mode) => {
+ this.updateStaticValueValidator(AlarmRuleConditionType.DURATION, mode);
+ });
+ this.repeatingDynamicModeControl.valueChanges.pipe(
+ takeUntilDestroyed()
+ ).subscribe((mode) => {
+ this.updateStaticValueValidator(AlarmRuleConditionType.REPEATING, mode);
+ });
+
+ this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
+ this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE');
+ }
+
+ updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) {
+ const control = this.conditionFormGroup.get(type === AlarmRuleConditionType.DURATION ? 'value' : 'count');
+ if (dynamicValue) {
+ control.get('staticValue').disable({emitEvent: false});
+ control.get('dynamicValueArgument').enable({emitEvent: false});
+ } else {
+ control.get('staticValue').enable({emitEvent: false});
+ control.get('dynamicValueArgument').disable({emitEvent: false});
+ }
+ }
+
+ updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') {
+ if (type === 'SIMPLE') {
+ this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false});
+ this.conditionFormGroup.get(`expression.filters`).enable({emitEvent: false});
+ } else {
+ this.conditionFormGroup.get(`expression.expression`).enable({emitEvent: false});
+ this.conditionFormGroup.get(`expression.filters`).disable({emitEvent: false});
+ }
+ }
+
+ private updateValidators(type: AlarmRuleConditionType, emitEvent = false) {
+ switch (type) {
+ case AlarmRuleConditionType.DURATION:
+ this.conditionFormGroup.get('unit').enable({emitEvent: false});
+ this.conditionFormGroup.get('value').enable({emitEvent: false});
+ this.conditionFormGroup.get('count').disable({emitEvent: false});
+ this.updateStaticValueValidator(type, this.durationDynamicModeControl.value);
+ this.defaultValuePlaceholder = 'alarm-rule.condition-duration-value';
+ this.defaultValueRequiredError = 'alarm-rule.condition-duration-value-required';
+ this.defaultValueRangeError = 'alarm-rule.condition-duration-value-range';
+ this.defaultValuePatternError = 'alarm-rule.condition-duration-value-pattern';
+ break;
+ case AlarmRuleConditionType.REPEATING:
+ this.conditionFormGroup.get('count').enable({emitEvent: false});
+ this.conditionFormGroup.get('value').disable({emitEvent: false});
+ this.conditionFormGroup.get('unit').disable({emitEvent: false});
+ this.updateStaticValueValidator(type, this.repeatingDynamicModeControl.value);
+ this.defaultValuePlaceholder = 'alarm-rule.condition-repeating-value';
+ this.defaultValueRequiredError = 'alarm-rule.condition-repeating-value-required';
+ this.defaultValueRangeError = 'alarm-rule.condition-repeating-value-range';
+ this.defaultValuePatternError = 'alarm-rule.condition-repeating-value-pattern';
+ break;
+ case AlarmRuleConditionType.SIMPLE:
+ this.conditionFormGroup.get('value').disable({emitEvent: false});
+ this.conditionFormGroup.get('count').disable({emitEvent: false});
+ this.conditionFormGroup.get('unit').disable({emitEvent: false});
+ break;
+ }
+ }
+
+ cancel(): void {
+ this.dialogRef.close(null);
+ }
+
+ save(): void {
+ this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition);
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
new file mode 100644
index 0000000000..e17d8c8261
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
@@ -0,0 +1,52 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss
new file mode 100644
index 0000000000..ac77089857
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2025 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: flex;
+ flex: 1;
+
+ .tb-alarm-rule-condition {
+ display: flex;
+ flex: 1;
+ &-button {
+ --mat-outlined-button-horizontal-padding: 3px 0px 12px;
+ display: block;
+ width: 100%;
+ }
+ &-label {
+ display: block;
+ text-align: start;
+ width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
new file mode 100644
index 0000000000..17ed11d892
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
@@ -0,0 +1,281 @@
+///
+/// Copyright © 2016-2025 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, forwardRef, Input } from '@angular/core';
+import {
+ ControlValueAccessor,
+ FormBuilder,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ UntypedFormControl,
+ Validator,
+ Validators
+} from '@angular/forms';
+import { MatDialog } from '@angular/material/dialog';
+import { deepClone, isDefinedAndNotNull } from '@core/utils';
+import { TranslateService } from '@ngx-translate/core';
+import {
+ dayOfWeekTranslations,
+ getAlarmScheduleRangeText,
+ utcTimestampToTimeOfDay
+} from '@shared/models/device.models';
+import { TimeUnit } from '@shared/models/time/time.models';
+import {
+ CfAlarmRuleConditionDialogComponent,
+ CfAlarmRuleConditionDialogData
+} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component";
+import {
+ AlarmRuleCondition,
+ AlarmRuleConditionType,
+ AlarmRuleExpressionType,
+ AlarmRuleSchedule,
+ AlarmRuleScheduleType
+} from "@shared/models/alarm-rule.models";
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+import {
+ AlarmRuleScheduleDialogData,
+ CfAlarmScheduleDialogComponent
+} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component";
+import { coerceBoolean } from "@shared/decorators/coercion";
+
+@Component({
+ selector: 'tb-cf-alarm-rule-condition',
+ templateUrl: './cf-alarm-rule-condition.component.html',
+ styleUrls: ['./cf-alarm-rule-condition.component.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => CfAlarmRuleConditionComponent),
+ multi: true
+ },
+ {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => CfAlarmRuleConditionComponent),
+ multi: true,
+ }
+ ]
+})
+export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator {
+
+ @Input()
+ @coerceBoolean()
+ disabled: boolean;
+
+ @Input()
+ arguments: Record;
+
+ alarmRuleConditionFormGroup = this.fb.group({
+ type: ['SIMPLE'],
+ expression: [{type: AlarmRuleExpressionType.SIMPLE}, Validators.required],
+ schedule: [null],
+ });
+
+ specText = '';
+
+ scheduleText = '';
+
+ private modelValue: AlarmRuleCondition;
+
+ private propagateChange = (v: any) => { };
+
+ constructor(private dialog: MatDialog,
+ private fb: FormBuilder,
+ private cd: ChangeDetectorRef,
+ private translate: TranslateService) {
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.alarmRuleConditionFormGroup.disable({emitEvent: false});
+ } else {
+ this.alarmRuleConditionFormGroup.enable({emitEvent: false});
+ }
+ }
+
+ writeValue(value: AlarmRuleCondition): void {
+ this.modelValue = value;
+ this.updateConditionInfo();
+ }
+
+ public conditionSet() {
+ return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters);
+ }
+
+ public validate(c: UntypedFormControl) {
+ return this.conditionSet() ? null : {
+ alarmRuleCondition: {
+ valid: false,
+ },
+ };
+ }
+
+ public openFilterDialog($event: Event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ this.dialog.open(CfAlarmRuleConditionDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ readonly: this.disabled,
+ condition: this.disabled ? this.modelValue : deepClone(this.modelValue),
+ arguments: this.arguments
+ }
+ }).afterClosed().subscribe((result) => {
+ if (result) {
+ this.modelValue = {...this.modelValue, ...result};
+ this.updateModel();
+ this.cd.detectChanges();
+ }
+ });
+ }
+
+ private updateConditionInfo() {
+ this.alarmRuleConditionFormGroup.patchValue(
+ {
+ type: this.modelValue?.type,
+ expression: this.modelValue?.expression,
+ schedule: this.modelValue?.schedule,
+ }, {emitEvent: false}
+ );
+ this.updateScheduleText();
+ this.updateSpecText();
+ }
+
+ private updateSpecText() {
+ this.specText = '';
+ if (this.modelValue && this.modelValue.type) {
+ const type = this.modelValue.type;
+ switch (type) {
+ case AlarmRuleConditionType.SIMPLE:
+ break;
+ case AlarmRuleConditionType.DURATION:
+ let duringText = '';
+ switch (this.modelValue.unit) {
+ case TimeUnit.SECONDS:
+ duringText = this.translate.instant('timewindow.seconds', {seconds: this.modelValue.value.staticValue});
+ break;
+ case TimeUnit.MINUTES:
+ duringText = this.translate.instant('timewindow.minutes', {minutes: this.modelValue.value.staticValue});
+ break;
+ case TimeUnit.HOURS:
+ duringText = this.translate.instant('timewindow.hours', {hours: this.modelValue.value.staticValue});
+ break;
+ case TimeUnit.DAYS:
+ duringText = this.translate.instant('timewindow.days', {days: this.modelValue.value.staticValue});
+ break;
+ }
+ if (this.modelValue.value.dynamicValueArgument) {
+ this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', {
+ attribute: `${this.modelValue.value.dynamicValueArgument}`
+ });
+ } else {
+ this.specText = this.translate.instant('alarm-rule.condition-during', {
+ during: duringText
+ });
+ }
+ break;
+ case AlarmRuleConditionType.REPEATING:
+ if (this.modelValue.count.dynamicValueArgument) {
+ this.specText = this.translate.instant('alarm-rule.condition-repeat-times-dynamic', {
+ attribute: `${this.modelValue.count.dynamicValueArgument}`
+ });
+ } else {
+ this.specText = this.translate.instant('alarm-rule.condition-repeat-times',
+ {count: this.modelValue.count.staticValue});
+ }
+ break;
+ }
+ }
+ }
+
+ private updateModel() {
+ this.updateConditionInfo();
+ this.propagateChange(this.modelValue);
+ }
+
+ public openScheduleDialog($event: Event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ this.dialog.open(CfAlarmScheduleDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ readonly: this.disabled,
+ alarmSchedule: this.disabled ? this.modelValue?.schedule : deepClone(this.modelValue?.schedule),
+ arguments: this.arguments
+ }
+ }).afterClosed().subscribe((result) => {
+ if (result) {
+ this.modelValue.schedule = result;
+ this.updateModel();
+ this.cd.detectChanges();
+ }
+ });
+ }
+
+ private updateScheduleText() {
+ let schedule = this.modelValue?.schedule;
+ this.scheduleText = '';
+ if (isDefinedAndNotNull(schedule)) {
+ if (schedule.dynamicValueArgument) {
+ this.scheduleText = this.translate.instant('alarm-rule.value-argument') + ': ' + schedule?.dynamicValueArgument
+ } else {
+ switch (schedule.staticValue.type) {
+ case AlarmRuleScheduleType.ANY_TIME:
+ this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time');
+ break;
+ case AlarmRuleScheduleType.SPECIFIC_TIME:
+ for (const day of schedule.staticValue.daysOfWeek) {
+ if (this.scheduleText.length) {
+ this.scheduleText += ', ';
+ }
+ this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]);
+ }
+ this.scheduleText += ' ' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.staticValue.startsOn),
+ utcTimestampToTimeOfDay(schedule.staticValue.endsOn)) + '';
+ break;
+ case AlarmRuleScheduleType.CUSTOM:
+ for (const item of schedule.staticValue.items) {
+ if (item.enabled) {
+ if (this.scheduleText.length) {
+ this.scheduleText += ', ';
+ }
+ this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]);
+ this.scheduleText += ' ' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn),
+ utcTimestampToTimeOfDay(item.endsOn)) + '';
+ }
+ }
+ break;
+ }
+ }
+ }
+ if (!this.scheduleText.length) {
+ this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time');
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
new file mode 100644
index 0000000000..e1f2f41bfd
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
@@ -0,0 +1,50 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss
new file mode 100644
index 0000000000..21b8a67c24
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2025 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+:host {
+ .tb-alarm-rule-details, .tb-alarm-rule-dashboard {
+ padding: 4px;
+ &.title {
+ opacity: 0.7;
+ overflow: visible;
+ }
+ }
+ .tb-alarm-rule-details {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ cursor: pointer;
+ }
+}
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts
new file mode 100644
index 0000000000..88ae1dd79d
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts
@@ -0,0 +1,149 @@
+///
+/// Copyright © 2016-2025 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, forwardRef, Input, OnInit } from '@angular/core';
+import {
+ ControlValueAccessor,
+ FormBuilder,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ UntypedFormControl,
+ Validator,
+ Validators
+} from '@angular/forms';
+import { MatDialog } from '@angular/material/dialog';
+import { isDefinedAndNotNull } from '@core/utils';
+import { DashboardId } from '@shared/models/id/dashboard-id';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { AlarmRule } from "@shared/models/alarm-rule.models";
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+import {
+ AlarmRuleDetailsDialogComponent,
+ AlarmRuleDetailsDialogData
+} from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
+import { coerceBoolean } from "@shared/decorators/coercion";
+
+@Component({
+ selector: 'tb-cf-alarm-rule',
+ templateUrl: './cf-alarm-rule.component.html',
+ styleUrls: ['./cf-alarm-rule.component.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => CfAlarmRuleComponent),
+ multi: true
+ },
+ {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => CfAlarmRuleComponent),
+ multi: true,
+ }
+ ]
+})
+export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator {
+
+ @Input()
+ @coerceBoolean()
+ disabled: boolean;
+
+ @Input()
+ @coerceBoolean()
+ required: boolean;
+
+ @Input()
+ arguments: Record;
+
+ private modelValue: AlarmRule;
+
+ alarmRuleFormGroup = this.fb.group({
+ condition: this.fb.control({}, Validators.required),
+ alarmDetails: [null],
+ dashboardId: [null]
+ });
+
+ private propagateChange = (v: any) => { };
+
+ constructor(private dialog: MatDialog,
+ private fb: FormBuilder,
+ private destroyRef: DestroyRef) {
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ ngOnInit() {
+ this.alarmRuleFormGroup.valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe(() => {
+ this.updateModel();
+ });
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.alarmRuleFormGroup.disable({emitEvent: false});
+ } else {
+ this.alarmRuleFormGroup.enable({emitEvent: false});
+ }
+ }
+
+ writeValue(value: AlarmRule): void {
+ this.modelValue = value;
+ const model = this.modelValue ? {
+ ...this.modelValue,
+ dashboardId: this.modelValue.dashboardId?.id
+ } : null;
+ this.alarmRuleFormGroup.patchValue(model, {emitEvent: false});
+ }
+
+ public openEditDetailsDialog($event: Event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ this.dialog.open(AlarmRuleDetailsDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value,
+ readonly: this.disabled
+ }
+ }).afterClosed().subscribe((alarmDetails) => {
+ if (isDefinedAndNotNull(alarmDetails)) {
+ this.alarmRuleFormGroup.patchValue({alarmDetails});
+ }
+ });
+ }
+
+ public validate(c: UntypedFormControl) {
+ return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : {
+ alarmRule: {
+ valid: false,
+ },
+ };
+ }
+
+ private updateModel() {
+ const value = this.alarmRuleFormGroup.value;
+ this.modelValue = {...value, dashboardId: value.dashboardId ? new DashboardId(value.dashboardId) : null} as AlarmRule;
+ this.propagateChange(this.modelValue);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss
new file mode 100644
index 0000000000..14df9a3972
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss
@@ -0,0 +1,60 @@
+/**
+ * Copyright © 2016-2025 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 {
+ form {
+ width: 900px;
+ max-width: 100%;
+ display: grid;
+ grid-template-rows: min-content minmax(auto, 1fr) min-content;
+ }
+
+ .tbel-script-lang-chip {
+ line-height: 20px;
+ font-size: 14px;
+ font-weight: 500;
+ color: white;
+ border-radius: 100px;
+ width: 70px;
+ min-width: 70px;
+ display: flex;
+ justify-content: center;
+ margin-top: 2px;
+ margin-right: 4px;
+ }
+
+ .tb-js-func {
+ .ace_tb {
+ &.ace_calculated-field {
+ &-ctx {
+ color: #C52F00;
+ }
+ &-args {
+ color: #185F2A;
+ }
+ &-key {
+ color: #c24c1a;
+ }
+ &-time-window, &-values, &-func, &-value, &-ts, &-latestTs {
+ color: #7214D0;
+ }
+ &-start-ts, &-end-ts {
+ color: #2CAA00;
+ }
+ }
+ }
+ }
+}
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html
new file mode 100644
index 0000000000..2d12355f48
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html
@@ -0,0 +1,53 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts
new file mode 100644
index 0000000000..e132dd499e
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts
@@ -0,0 +1,70 @@
+///
+/// Copyright © 2016-2025 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, Inject } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { FormBuilder } from '@angular/forms';
+import { Router } from '@angular/router';
+import { DialogComponent } from '@app/shared/components/dialog.component';
+import { AlarmRuleSchedule } from "@shared/models/alarm-rule.models";
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+
+export interface AlarmRuleScheduleDialogData {
+ readonly: boolean;
+ alarmSchedule: AlarmRuleSchedule;
+ arguments: Record;
+}
+
+@Component({
+ selector: 'tb-cf-alarm-schedule-dialog',
+ templateUrl: './cf-alarm-schedule-dialog.component.html',
+ providers: [],
+ styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
+})
+export class CfAlarmScheduleDialogComponent extends DialogComponent{
+
+ readonly = this.data.readonly;
+ alarmSchedule = this.data.alarmSchedule;
+ arguments = this.data.arguments;
+
+ alarmScheduleControl = this.fb.control(null);
+
+ dynamicModeControl = this.fb.control(false);
+
+ constructor(protected store: Store,
+ protected router: Router,
+ @Inject(MAT_DIALOG_DATA) public data: AlarmRuleScheduleDialogData,
+ public dialogRef: MatDialogRef,
+ private fb: FormBuilder) {
+ super(store, router, dialogRef);
+ this.alarmScheduleControl.patchValue(this.alarmSchedule, {emitEvent: false});
+ this.dynamicModeControl.patchValue(!!this.alarmSchedule?.dynamicValueArgument, {emitEvent: false});
+ if (this.readonly) {
+ this.alarmScheduleControl.disable({emitEvent: false});
+ this.dynamicModeControl.disable({emitEvent: false});
+ }
+ }
+
+ cancel(): void {
+ this.dialogRef.close(null);
+ }
+
+ save(): void {
+ this.dialogRef.close(this.alarmScheduleControl.value);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html
new file mode 100644
index 0000000000..13c158e67d
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html
@@ -0,0 +1,125 @@
+
+
+ @if (!dynamicMode) {
+
+
+
+ @for (scheduleType of alarmScheduleTypes; track scheduleType) {
+ {{ alarmScheduleTypeTranslate.get(scheduleType) | translate }}
+ }
+
+ @if (alarmScheduleForm.get('staticValue.type').hasError('required')) {
+ {{ 'alarm-rule.schedule-type-required' | translate }}
+ }
+
+
+ @if (alarmScheduleForm.get('staticValue.type').value !== alarmScheduleType.ANY_TIME) {
+
+ }
+ } @else {
+
+
+ alarm-rule.value-argument
+
+ @for (argument of argumentsList; track argument) {
+ {{ argument }}
+ }
+
+ @if (alarmScheduleForm.get('dynamicValueArgument').hasError('required')) {
+ {{ 'calculated-fields.hint.argument-name-required' | translate }}
+ }
+
+
+
+
+ }
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts
new file mode 100644
index 0000000000..b1c7a4da7f
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts
@@ -0,0 +1,322 @@
+///
+/// Copyright © 2016-2025 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, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
+import {
+ AbstractControl,
+ ControlValueAccessor,
+ FormArray,
+ FormBuilder,
+ FormGroup,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator,
+ Validators
+} from '@angular/forms';
+import {
+ CustomTimeSchedulerItem,
+ dayOfWeekTranslations,
+ getAlarmScheduleRangeText,
+ timeOfDayToUTCTimestamp,
+ utcTimestampToTimeOfDay
+} from '@shared/models/device.models';
+import { isDefined } from '@core/utils';
+import { getDefaultTimezone } from '@shared/models/time/time.models';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import {
+ AlarmRuleSchedule,
+ AlarmRuleScheduleType,
+ AlarmRuleScheduleTypeTranslationMap
+} from "@shared/models/alarm-rule.models";
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+import { MatChipSelectionChange } from "@angular/material/chips";
+import { coerceBoolean } from "@shared/decorators/coercion";
+
+@Component({
+ selector: 'tb-cf-alarm-schedule',
+ templateUrl: './cf-alarm-schedule.component.html',
+ styleUrls: [],
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => CfAlarmScheduleComponent),
+ multi: true
+ }, {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => CfAlarmScheduleComponent),
+ multi: true
+ }]
+})
+export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
+
+ @Input()
+ @coerceBoolean()
+ disabled: boolean;
+
+ @Input()
+ arguments: Record;
+
+ @Input()
+ @coerceBoolean()
+ dynamicMode: boolean;
+
+ alarmScheduleForm = this.fb.group({
+ staticValue: this.fb.group({
+ type: [AlarmRuleScheduleType.ANY_TIME, Validators.required],
+ timezone: ['', Validators.required],
+ daysOfWeek: this.fb.control(null, Validators.required),
+ startsOn: this.fb.control(0, Validators.required),
+ endsOn: this.fb.control(0, Validators.required),
+ items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems),
+ }),
+ dynamicValueArgument: ['', Validators.required]
+ });
+
+ alarmScheduleTypes = Object.keys(AlarmRuleScheduleType) as Array;
+ alarmScheduleType = AlarmRuleScheduleType;
+ alarmScheduleTypeTranslate = AlarmRuleScheduleTypeTranslationMap;
+ dayOfWeekTranslationsArray = dayOfWeekTranslations;
+
+ argumentsList: Array;
+
+ allDays = Array(7).fill(0).map((x, i) => i);
+
+ private modelValue: AlarmRuleSchedule;
+
+ private defaultItems = Array.from({length: 7}, (value, i) => ({
+ enabled: true,
+ dayOfWeek: i + 1
+ })) as CustomTimeSchedulerItem[];
+
+ private propagateChange = (v: any) => { };
+
+ constructor(private fb: FormBuilder,
+ private destroyRef: DestroyRef) {
+ }
+
+ ngOnInit(): void {
+ this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
+ this.alarmScheduleForm.get('staticValue.type').valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe((type) => {
+ const defaultTimezone = getDefaultTimezone();
+ const staticValue = {...this.alarmScheduleForm.get('staticValue').value, type, items: this.defaultItems, timezone: defaultTimezone};
+ this.alarmScheduleForm.get('staticValue').patchValue(staticValue, {emitEvent: false});
+ this.alarmScheduleForm.get('dynamicValueArgument').patchValue(null, {emitEvent: false});
+ this.updateValidators(type);
+ });
+ this.alarmScheduleForm.valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe(() => {
+ this.updateModel();
+ });
+
+ this.alarmScheduleForm.get('staticValue.items').valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe((items) => {
+ items.forEach((item, index) => this.disabledSelectedTime(item.enabled, index, false))
+ });
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.dynamicMode) {
+ const dynamicModeChanges = changes.dynamicMode;
+ if (!dynamicModeChanges.firstChange && dynamicModeChanges.currentValue !== dynamicModeChanges.previousValue) {
+ this.updateModeValidators(dynamicModeChanges.currentValue);
+ this.updateModel();
+ }
+ }
+ }
+
+ validateItems(control: AbstractControl): ValidationErrors | null {
+ const items: any[] = control.value;
+ if (!items || !items.length || !items.find(v => v.enabled === true)) {
+ return {
+ dayOfWeeks: true
+ };
+ }
+ return null;
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.alarmScheduleForm.disable({emitEvent: false});
+ } else {
+ this.updateModeValidators(this.dynamicMode);
+ }
+ }
+
+ writeValue(value: AlarmRuleSchedule): void {
+ if (value) {
+ this.modelValue = value;
+ if (this.modelValue.dynamicValueArgument) {
+ this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false});
+ } else {
+ switch (this.modelValue.staticValue.type) {
+ case AlarmRuleScheduleType.SPECIFIC_TIME:
+ this.alarmScheduleForm.patchValue({
+ staticValue: {
+ type: this.modelValue.staticValue.type,
+ timezone: this.modelValue.staticValue.timezone,
+ daysOfWeek: this.modelValue.staticValue.daysOfWeek,
+ startsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.startsOn),
+ endsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.endsOn),
+ },
+ }, {emitEvent: false});
+ break;
+ case AlarmRuleScheduleType.CUSTOM:
+ if (this.modelValue?.dynamicValueArgument) {
+ this.alarmScheduleForm.patchValue({
+ staticValue: {
+ type: this.modelValue.staticValue.type,
+ },
+ }, {emitEvent: false});
+ } else if (this.modelValue.staticValue?.items) {
+ const alarmDays = [];
+ this.modelValue.staticValue.items
+ .sort((a, b) => a.dayOfWeek - b.dayOfWeek)
+ .forEach((item, index) => {
+ this.disabledSelectedTime(item.enabled, index);
+ alarmDays.push({
+ enabled: item.enabled,
+ startsOn: utcTimestampToTimeOfDay(item.startsOn),
+ endsOn: utcTimestampToTimeOfDay(item.endsOn)
+ });
+ });
+ this.alarmScheduleForm.patchValue({
+ staticValue: {
+ type: this.modelValue.staticValue.type,
+ timezone: this.modelValue.staticValue.timezone,
+ items: alarmDays,
+ },
+ }, {emitEvent: false});
+ }
+ break;
+ default:
+ this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false});
+ }
+ this.updateValidators(this.modelValue.staticValue.type);
+ }
+ this.updateModeValidators(this.dynamicMode);
+ }
+ }
+
+ validate(control: FormGroup): ValidationErrors | null {
+ return this.alarmScheduleForm.valid ? null : {
+ alarmScheduler: {
+ valid: false
+ }
+ };
+ }
+
+ private updateModeValidators(mode: boolean) {
+ if (mode) {
+ this.alarmScheduleForm.get('staticValue').disable({emitEvent: false});
+ this.alarmScheduleForm.get('dynamicValueArgument').enable({emitEvent: false});
+ } else {
+ this.alarmScheduleForm.get('staticValue').enable({emitEvent: false});
+ this.alarmScheduleForm.get('dynamicValueArgument').disable({emitEvent: false});
+ this.updateValidators(this.alarmScheduleForm.get('staticValue.type').value);
+ }
+ }
+
+ private updateValidators(type: AlarmRuleScheduleType){
+ switch (type){
+ case AlarmRuleScheduleType.ANY_TIME:
+ this.alarmScheduleForm.get('staticValue.timezone').disable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false});
+ break;
+ case AlarmRuleScheduleType.SPECIFIC_TIME:
+ this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.daysOfWeek').enable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.startsOn').enable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.endsOn').enable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false});
+ break;
+ case AlarmRuleScheduleType.CUSTOM:
+ this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false});
+ this.alarmScheduleForm.get('staticValue.items').enable({emitEvent: false});
+ break;
+ }
+ }
+
+ private updateModel() {
+ const value = this.alarmScheduleForm.value as AlarmRuleSchedule;
+ if (!this.dynamicMode) {
+ if (isDefined(value.staticValue.startsOn) && value.staticValue.startsOn !== 0) {
+ value.staticValue.startsOn = timeOfDayToUTCTimestamp(value.staticValue.startsOn);
+ }
+ if (isDefined(value.staticValue.endsOn) && value.staticValue.endsOn !== 0) {
+ value.staticValue.endsOn = timeOfDayToUTCTimestamp(value.staticValue.endsOn);
+ }
+ if (isDefined(value.staticValue.items)){
+ value.staticValue.items = this.alarmScheduleForm.getRawValue().staticValue.items as CustomTimeSchedulerItem[];
+ value.staticValue.items = value.staticValue.items.map((item) => {
+ return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)};
+ });
+ }
+ }
+ this.modelValue = value;
+ this.propagateChange(this.modelValue);
+ }
+
+
+ private defaultItemsScheduler(index: number): FormGroup {
+ return this.fb.group({
+ enabled: [true],
+ dayOfWeek: [index + 1],
+ startsOn: [0, Validators.required],
+ endsOn: [0, Validators.required]
+ });
+ }
+
+ changeCustomScheduler($event: MatChipSelectionChange, index: number) {
+ const value = $event.selected;
+ this.disabledSelectedTime(value, index, true);
+ }
+
+ private disabledSelectedTime(enable: boolean, index: number, emitEvent = false) {
+ if (enable) {
+ this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false});
+ this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent});
+ } else {
+ this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false});
+ this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent});
+ }
+ }
+
+ getSchedulerRangeText(control: FormGroup | AbstractControl): string {
+ return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value);
+ }
+
+ get itemsSchedulerForm(): FormArray {
+ return this.alarmScheduleForm.get('staticValue.items') as FormArray;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
new file mode 100644
index 0000000000..820418a6b0
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
@@ -0,0 +1,65 @@
+
+
+
+ @for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) {
+
+ }
+
+
+ alarm-rule.add-create-alarm-rule-prompt
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss
new file mode 100644
index 0000000000..c00cf6af9c
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016-2025 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 {
+ .create-alarm-rule {
+ border: 1px solid rgba(0, 0, 0, .12);
+ border-left-width: 4px;
+ border-radius: 4px;
+ padding: 8px;
+ min-width: 0;
+ }
+}
+
+:host ::ng-deep {
+ .mat-mdc-form-field.severity {
+ .mat-mdc-form-field-infix {
+ width: 160px;
+ }
+ }
+ .button-icon {
+ color: rgba(0, 0, 0, 0.38);
+ min-width: 40px;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
new file mode 100644
index 0000000000..1ffc52bb34
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
@@ -0,0 +1,187 @@
+///
+/// Copyright © 2016-2025 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, forwardRef, Input } from '@angular/core';
+import {
+ AbstractControl,
+ ControlValueAccessor,
+ FormArray,
+ FormBuilder,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ UntypedFormArray,
+ UntypedFormControl,
+ Validator,
+ Validators
+} from '@angular/forms';
+import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models';
+import { AlarmRule } from "@shared/models/alarm-rule.models";
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+import { AlarmSeverityNotificationColors } from "@shared/models/notification.models";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { coerceBoolean } from "@shared/decorators/coercion";
+
+@Component({
+ selector: 'tb-create-cf-alarm-rules',
+ templateUrl: './create-cf-alarm-rules.component.html',
+ styleUrls: ['./create-cf-alarm-rules.component.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => CreateCfAlarmRulesComponent),
+ multi: true
+ },
+ {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => CreateCfAlarmRulesComponent),
+ multi: true,
+ }
+ ]
+})
+export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Validator {
+
+ @Input()
+ @coerceBoolean()
+ disabled: boolean;
+
+ @Input()
+ arguments: Record;
+
+
+ alarmSeverities = Object.keys(AlarmSeverity);
+ alarmSeverityEnum = AlarmSeverity;
+ alarmSeverityTranslationMap = alarmSeverityTranslations;
+
+ AlarmSeverityNotificationColors = AlarmSeverityNotificationColors;
+
+ createAlarmRulesFormGroup = this.fb.group({
+ createAlarmRules: this.fb.array<{severity: AlarmSeverity, alarmRule: AlarmRule}>([])
+ });
+
+ private usedSeverities: AlarmSeverity[] = [];
+
+ private propagateChange = (v: any) => { };
+
+ constructor(private fb: FormBuilder,
+ private destroyRef: DestroyRef) {
+ this.createAlarmRulesFormGroup.valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe(() => this.updateModel());
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ createAlarmRulesFormArray(): UntypedFormArray {
+ return this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.createAlarmRulesFormGroup.disable({emitEvent: false});
+ } else {
+ this.createAlarmRulesFormGroup.enable({emitEvent: false});
+ }
+ }
+
+ writeValue(createAlarmRules: Record): void {
+ const createAlarmRulesControls: Array = [];
+ if (createAlarmRules) {
+ Object.keys(createAlarmRules).forEach((severity) => {
+ const createAlarmRule = createAlarmRules[severity];
+ if (severity === 'empty') {
+ severity = null;
+ }
+ createAlarmRulesControls.push(this.fb.group({
+ severity: [severity, Validators.required],
+ alarmRule: [createAlarmRule, Validators.required]
+ }));
+ });
+ }
+ const formArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray;
+ formArray.clear();
+ createAlarmRulesControls.forEach(c => formArray.push(c));
+ if (this.disabled) {
+ this.createAlarmRulesFormGroup.disable({emitEvent: false});
+ } else {
+ this.createAlarmRulesFormGroup.enable({emitEvent: false});
+ }
+ this.updateUsedSeverities();
+ if (!this.disabled && !this.createAlarmRulesFormGroup.valid) {
+ this.updateModel();
+ }
+ }
+
+ public removeCreateAlarmRule(index: number) {
+ (this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray).removeAt(index);
+ }
+
+ public addCreateAlarmRule() {
+ const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray;
+ createAlarmRulesArray.push(this.fb.group({
+ severity: [this.getFirstUnusedSeverity(), Validators.required],
+ alarmRule: [null, Validators.required]
+ }));
+ this.createAlarmRulesFormGroup.updateValueAndValidity();
+ if (!this.createAlarmRulesFormGroup.valid) {
+ this.updateModel();
+ }
+ }
+
+ private getFirstUnusedSeverity(): AlarmSeverity {
+ for (const severityKey of Object.keys(AlarmSeverity)) {
+ const severity = AlarmSeverity[severityKey];
+ if (this.usedSeverities.indexOf(severity) === -1) {
+ return severity;
+ }
+ }
+ return null;
+ }
+
+ public validate(c: UntypedFormControl) {
+ return (this.createAlarmRulesFormGroup.valid) ? null : {
+ createAlarmRules: {
+ valid: false,
+ },
+ };
+ }
+
+ public isDisabledSeverity(severity: AlarmSeverity, index: number): boolean {
+ const usedIndex = this.usedSeverities.indexOf(severity);
+ return usedIndex > -1 && usedIndex !== index;
+ }
+
+ private updateUsedSeverities() {
+ this.usedSeverities = [];
+ const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
+ value.forEach((rule, index) => {
+ this.usedSeverities[index] = AlarmSeverity[rule.severity];
+ });
+ }
+
+ private updateModel() {
+ const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
+ const createAlarmRules = {} as Record;
+ value.forEach(v => createAlarmRules[v.severity] = v.alarmRule);
+ this.updateUsedSeverities();
+ this.propagateChange(createAlarmRules);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html
new file mode 100644
index 0000000000..ed5dc4b9cd
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html
@@ -0,0 +1,55 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts
new file mode 100644
index 0000000000..dab366f960
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts
@@ -0,0 +1,87 @@
+///
+/// Copyright © 2016-2025 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, Inject } from '@angular/core';
+import { ErrorStateMatcher } from '@angular/material/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { FormBuilder, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+import { DialogComponent } from '@app/shared/components/dialog.component';
+import {
+ ComplexOperation,
+ complexOperationTranslationMap,
+ EntityKeyValueType,
+ FilterPredicateType
+} from '@shared/models/query/query.models';
+import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+
+export interface AlarmRuleComplexFilterPredicateDialogData {
+ complexPredicate: ComplexAlarmRuleFilterPredicate;
+ isAdd: boolean;
+ valueType: EntityKeyValueType;
+ arguments: Record;
+}
+
+@Component({
+ selector: 'tb-alarm-rule-complex-filter-predicate-dialog',
+ templateUrl: './alarm-rule-complex-filter-predicate-dialog.component.html',
+ providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleComplexFilterPredicateDialogComponent}],
+ styleUrls: []
+})
+
+export class AlarmRuleComplexFilterPredicateDialogComponent extends
+ DialogComponent {
+
+ complexFilterFormGroup = this.fb.group(
+ {
+ operation: [ComplexOperation.AND, [Validators.required]],
+ predicates: this.fb.control(null, Validators.required)
+ }
+ );
+
+ complexOperations = Object.keys(ComplexOperation);
+ complexOperationEnum = ComplexOperation;
+ complexOperationTranslations = complexOperationTranslationMap;
+
+ isAdd: boolean;
+
+ arguments = this.data.arguments;
+
+ constructor(protected store: Store,
+ protected router: Router,
+ @Inject(MAT_DIALOG_DATA) public data: AlarmRuleComplexFilterPredicateDialogData,
+ public dialogRef: MatDialogRef,
+ private fb: FormBuilder) {
+ super(store, router, dialogRef);
+
+ this.isAdd = this.data.isAdd;
+
+ this.complexFilterFormGroup.patchValue(this.data.complexPredicate, {emitEvent: false});
+ }
+
+ cancel(): void {
+ this.dialogRef.close(null);
+ }
+
+ save(): void {
+ const predicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate;
+ predicate.type = FilterPredicateType.COMPLEX;
+ this.dialogRef.close(predicate);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
new file mode 100644
index 0000000000..e622e9e4f8
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
@@ -0,0 +1,98 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss
new file mode 100644
index 0000000000..0ec7194f02
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2025 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 {
+ .mat-mdc-form-field.tb-value-type {
+ mat-select-trigger {
+ .mat-icon {
+ vertical-align: middle;
+ margin-right: 8px;
+ svg {
+ vertical-align: initial;
+ }
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts
new file mode 100644
index 0000000000..6961524bdd
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts
@@ -0,0 +1,120 @@
+ ///
+/// Copyright © 2016-2025 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 } from '@angular/core';
+ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+ import { Store } from '@ngrx/store';
+ import { AppState } from '@core/core.state';
+ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+ import { Router } from '@angular/router';
+ import { DialogComponent } from '@app/shared/components/dialog.component';
+ import {
+ ComplexOperation,
+ complexOperationTranslationMap,
+ EntityKeyValueType,
+ entityKeyValueTypesMap
+ } from '@shared/models/query/query.models';
+ import { DialogService } from '@core/services/dialog.service';
+ import { TranslateService } from '@ngx-translate/core';
+ import { AlarmRuleFilter, AlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
+ import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+ import { FormControlsFrom } from "@shared/models/tenant.model";
+ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+
+ export interface AlarmRuleFilterDialogData {
+ filter: AlarmRuleFilter;
+ isAdd: boolean;
+ arguments: Record;
+ usedArguments: Array;
+}
+
+@Component({
+ selector: 'tb-alarm-rule-filter-dialog',
+ templateUrl: './alarm-rule-filter-dialog.component.html',
+ providers: [],
+ styleUrls: ['./alarm-rule-filter-dialog.component.scss']
+})
+export class AlarmRuleFilterDialogComponent extends DialogComponent {
+
+ filterFormGroup: FormGroup>;
+
+ entityKeyValueTypesKeys = Object.keys(EntityKeyValueType);
+
+ entityKeyValueTypeEnum = EntityKeyValueType;
+
+ entityKeyValueTypes = entityKeyValueTypesMap;
+
+ complexOperationTranslationMap = complexOperationTranslationMap;
+
+ ComplexOperation = ComplexOperation;
+
+ arguments = this.data.arguments;
+ argumentsList: Array;
+
+ constructor(protected store: Store,
+ protected router: Router,
+ @Inject(MAT_DIALOG_DATA) public data: AlarmRuleFilterDialogData,
+ public dialogRef: MatDialogRef,
+ private dialogs: DialogService,
+ private translate: TranslateService,
+ private fb: FormBuilder,
+ private destroyRef: DestroyRef) {
+ super(store, router, dialogRef);
+
+ this.argumentsList = this.arguments ? Object.keys(this.arguments) : [];
+
+ this.filterFormGroup = this.fb.group(
+ {
+ argument: [this.data.filter.argument, [Validators.required]],
+ valueType: [this.data.filter.valueType ?? EntityKeyValueType.STRING, [Validators.required]],
+ predicates: [this.data.filter.predicates, [Validators.required]],
+ operation: [this.data.filter.operation ?? ComplexOperation.AND]
+ }
+ );
+ this.filterFormGroup.get('valueType').valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe((valueType: EntityKeyValueType) => {
+ const prevValueType: EntityKeyValueType = this.filterFormGroup.value.valueType;
+ const predicates: AlarmRuleFilterPredicate[] = this.filterFormGroup.get('predicates').value;
+ if (prevValueType && prevValueType !== valueType) {
+ if (predicates && predicates.length) {
+ this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'),
+ this.translate.instant('filter.key-value-type-change-message')).subscribe(
+ (result) => {
+ if (result) {
+ this.filterFormGroup.get('predicates').setValue([]);
+ } else {
+ this.filterFormGroup.get('valueType').setValue(prevValueType, {emitEvent: false});
+ }
+ }
+ );
+ }
+ }
+ });
+ }
+
+ argumentInUse(argument: string): boolean {
+ return this.data.usedArguments.includes(argument);
+ }
+
+ cancel(): void {
+ this.dialogRef.close(null);
+ }
+
+ save(): void {
+ this.dialogRef.close(this.filterFormGroup.value as AlarmRuleFilter);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
new file mode 100644
index 0000000000..3f588a3d70
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
@@ -0,0 +1,79 @@
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss
new file mode 100644
index 0000000000..8e74f373f5
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2025 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 {
+ .filter-title {
+ padding: 12px 0;
+ font-size: 14px;
+ font-weight: 500;
+ }
+ .filter-list {
+ overflow: auto;
+ max-height: 300px;
+ .no-data-found {
+ height: 50px;
+ }
+
+ &-divider {
+ border-top: 1px solid rgba(0, 0, 0, 0.12);
+ }
+ }
+ .filters-operation {
+ display: flex;
+ justify-content: center;
+ margin-top: -14px;
+ &-container {
+ background-color: white;
+ }
+ &-label {
+ font-weight: 500;
+ color: #00695C;
+ padding: 0 8px;
+ border-radius: 4px;
+ border: 1px solid rgba(#00695C, 0.32);
+ background-color: rgba(#00695C, 0.04);
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
new file mode 100644
index 0000000000..86b7520910
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
@@ -0,0 +1,172 @@
+///
+/// Copyright © 2016-2025 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, forwardRef, Input } from '@angular/core';
+import {
+ AbstractControl,
+ ControlValueAccessor,
+ FormArray,
+ FormBuilder,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator,
+ Validators
+} from '@angular/forms';
+import { Observable } from 'rxjs';
+import {
+ ComplexOperation,
+ complexOperationTranslationMap,
+ EntityKeyValueType
+} from '@shared/models/query/query.models';
+import { MatDialog } from '@angular/material/dialog';
+import { deepClone } from '@core/utils';
+import {
+ AlarmRuleFilterDialogComponent,
+ AlarmRuleFilterDialogData
+} from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component";
+import { AlarmRuleFilter, FilterPredicateTypeTranslationMap } from "@shared/models/alarm-rule.models";
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+
+@Component({
+ selector: 'tb-alarm-rule-filter-list',
+ templateUrl: './alarm-rule-filter-list.component.html',
+ styleUrls: ['./alarm-rule-filter-list.component.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => AlarmRuleFilterListComponent),
+ multi: true
+ },
+ {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => AlarmRuleFilterListComponent),
+ multi: true
+ }
+ ]
+})
+export class AlarmRuleFilterListComponent implements ControlValueAccessor, Validator {
+
+ @Input()
+ arguments: Record;
+
+ @Input()
+ operation: ComplexOperation = ComplexOperation.AND;
+
+ filterListFormGroup = this.fb.group({
+ filters: this.fb.array([])
+ });
+
+ complexOperationTranslationMap = complexOperationTranslationMap;
+ FilterPredicateTypeTranslationMap = FilterPredicateTypeTranslationMap
+
+ private propagateChange = (v: any) => { };
+
+ constructor(private fb: FormBuilder,
+ private dialog: MatDialog,
+ private destroyRef: DestroyRef) {
+ this.filterListFormGroup.valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe(() => this.updateModel());
+ }
+
+
+ get filtersFormArray(): FormArray {
+ return this.filterListFormGroup.get('filters') as FormArray;
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ validate(): ValidationErrors | null {
+ return this.filterListFormGroup.valid && this.filterListFormGroup.get('filters').value?.length ? null : {
+ filterList: {valid: false}
+ };
+ }
+
+ writeValue(filters: Array): void {
+ const keyFilterControls: Array = [];
+ if (filters) {
+ for (const filter of filters) {
+ keyFilterControls.push(this.fb.control(filter, [Validators.required]));
+ }
+ }
+ this.filtersFormArray.clear();
+ keyFilterControls.forEach(c => this.filtersFormArray.push(c));
+ }
+
+ public removeFilter(index: number) {
+ (this.filterListFormGroup.get('filters') as FormArray).removeAt(index);
+ }
+
+ public addFilter() {
+ const filtersFormArray = this.filterListFormGroup.get('filters') as FormArray;
+ this.openFilterDialog(null).subscribe(result => {
+ if (result) {
+ filtersFormArray.push(this.fb.control(result, [Validators.required]));
+ }
+ });
+ }
+
+ public editFilter(index: number) {
+ const filter: AlarmRuleFilter =
+ (this.filterListFormGroup.get('filters') as FormArray).at(index).value;
+ this.openFilterDialog(filter).subscribe(result => {
+ if (result) {
+ (this.filterListFormGroup.get('filters') as FormArray).at(index).patchValue(result);
+ }
+ });
+ }
+
+ private openFilterDialog(filter?: AlarmRuleFilter): Observable {
+ const isAdd = !filter;
+ if (isAdd) {
+ filter = {
+ argument: null,
+ valueType: EntityKeyValueType.STRING,
+ operation: ComplexOperation.AND,
+ predicates: []
+ };
+ }
+ return this.dialog.open(AlarmRuleFilterDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ filter: filter ? deepClone(filter) : null,
+ isAdd,
+ arguments: this.arguments,
+ usedArguments: this.getUsedArguments
+ }
+ }).afterClosed();
+ }
+
+ get getUsedArguments(): Array {
+ const filters = this.filterListFormGroup.get('filters').value as Array;
+ return filters.length ? filters.map((filter: AlarmRuleFilter) => filter.argument) : [];
+ }
+
+ private updateModel() {
+ const filters = this.filterListFormGroup.value.filters as Array;
+ this.propagateChange(filters);
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
new file mode 100644
index 0000000000..210f3f9424
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss
new file mode 100644
index 0000000000..054d0e5281
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2025 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 {
+ .filter-title {
+ padding: 12px 8px;
+ font-size: 14px;
+ font-weight: 500;
+ }
+ .predicate-list {
+ .no-data-found {
+ height: 50px;
+ }
+ }
+
+ .key-filter-list-divider {
+ border-top: 1px solid rgba(0, 0, 0, 0.12);
+ }
+ .filters-operation {
+ display: flex;
+ justify-content: center;
+ margin-top: -14px;
+ &-container {
+ position: absolute;
+ top: -12px;
+ left: 10px;
+ background-color: white;
+ }
+ &-label {
+ font-weight: 500;
+ color: #00695C;
+ padding: 0 8px;
+ border-radius: 4px;
+ border: 1px solid rgba(#00695C, 0.32);
+ background-color: rgba(#00695C, 0.04);
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts
new file mode 100644
index 0000000000..2b7458ba46
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts
@@ -0,0 +1,207 @@
+///
+/// Copyright © 2016-2025 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, forwardRef, Input } from '@angular/core';
+import {
+ AbstractControl,
+ ControlValueAccessor,
+ FormArray,
+ FormBuilder,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator,
+ Validators
+} from '@angular/forms';
+import { Observable, of } from 'rxjs';
+import {
+ BooleanOperation,
+ ComplexOperation,
+ complexOperationTranslationMap,
+ EntityKeyValueType,
+ entityKeyValueTypeToFilterPredicateType,
+ FilterPredicateType,
+ NumericOperation,
+ StringOperation
+} from '@shared/models/query/query.models';
+import { MatDialog } from '@angular/material/dialog';
+import { map } from 'rxjs/operators';
+import {
+ AlarmRuleComplexFilterPredicateDialogComponent,
+ AlarmRuleComplexFilterPredicateDialogData
+} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component";
+import {
+ AlarmRuleFilterPredicate,
+ AlarmRulePredicateInfo,
+ ComplexAlarmRuleFilterPredicate
+} from "@shared/models/alarm-rule.models";
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+
+@Component({
+ selector: 'tb-alarm-rule-filter-predicate-list',
+ templateUrl: './alarm-rule-filter-predicate-list.component.html',
+ styleUrls: ['./alarm-rule-filter-predicate-list.component.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent),
+ multi: true
+ },
+ {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent),
+ multi: true
+ }
+ ]
+})
+export class AlarmRuleFilterPredicateListComponent implements ControlValueAccessor, Validator {
+
+ @Input() disabled: boolean;
+
+ @Input() valueType: EntityKeyValueType;
+
+ @Input() operation: ComplexOperation = ComplexOperation.AND;
+
+ @Input() arguments: Record;
+
+ filterListFormGroup = this.fb.group({
+ predicates: this.fb.array([])
+ });
+
+ valueTypeEnum = EntityKeyValueType;
+
+ complexOperationTranslations = complexOperationTranslationMap;
+
+ private propagateChange= (v: any) => { };
+
+ constructor(private fb: FormBuilder,
+ private dialog: MatDialog,
+ private destroyRef: DestroyRef) {
+ this.filterListFormGroup.valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe(() => this.updateModel());
+ }
+
+ get predicatesFormArray(): FormArray {
+ return this.filterListFormGroup.get('predicates') as FormArray;
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ setDisabledState?(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.filterListFormGroup.disable({emitEvent: false});
+ } else {
+ this.filterListFormGroup.enable({emitEvent: false});
+ }
+ }
+
+ validate(control: AbstractControl): ValidationErrors | null {
+ return this.filterListFormGroup.valid ? null : {
+ filterList: {valid: false}
+ };
+ }
+
+ writeValue(predicates: Array): void {
+ const predicateControls: Array = [];
+ if (predicates) {
+ for (const predicate of predicates) {
+ predicateControls.push(this.fb.control(predicate, [Validators.required]));
+ }
+ }
+ this.predicatesFormArray.clear();
+ predicateControls.forEach(predicate => this.predicatesFormArray.push(predicate));
+ }
+
+ public removePredicate(index: number) {
+ this.predicatesFormArray.removeAt(index);
+ }
+
+ public addPredicate(complex: boolean) {
+ const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray;
+ const predicate = this.createDefaultFilterPredicate(this.valueType, complex);
+ let observable: Observable;
+ if (complex) {
+ observable = this.openComplexFilterDialog(predicate as ComplexAlarmRuleFilterPredicate);
+ } else {
+ observable = of(predicate);
+ }
+ observable.subscribe((result) => {
+ if (result) {
+ predicatesFormArray.push(this.fb.control(result, [Validators.required]));
+ }
+ });
+ }
+
+ private createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): AlarmRuleFilterPredicate {
+ const predicate = {
+ type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType)
+ } as AlarmRuleFilterPredicate;
+ switch (predicate.type) {
+ case FilterPredicateType.STRING:
+ predicate.operation = StringOperation.STARTS_WITH;
+ predicate.value = {
+ staticValue: ''
+ };
+ predicate.ignoreCase = false;
+ break;
+ case FilterPredicateType.NUMERIC:
+ predicate.operation = NumericOperation.EQUAL;
+ predicate.value = {
+ staticValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0
+ };
+ break;
+ case FilterPredicateType.BOOLEAN:
+ predicate.operation = BooleanOperation.EQUAL;
+ predicate.value = {
+ staticValue: false
+ };
+ break;
+ case FilterPredicateType.COMPLEX:
+ predicate.operation = ComplexOperation.AND;
+ predicate.predicates = [];
+ break;
+ }
+ return predicate;
+ }
+
+ private openComplexFilterDialog(predicate: ComplexAlarmRuleFilterPredicate): Observable {
+ return this.dialog.open(AlarmRuleComplexFilterPredicateDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ complexPredicate: predicate as ComplexAlarmRuleFilterPredicate,
+ valueType: this.valueType,
+ isAdd: true,
+ arguments: this.arguments,
+ }
+ }).afterClosed().pipe(
+ map(result => result)
+ );
+ }
+
+ private updateModel() {
+ this.propagateChange(this.filterListFormGroup.get('predicates').value);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html
new file mode 100644
index 0000000000..c7bcc92012
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html
@@ -0,0 +1,73 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts
new file mode 100644
index 0000000000..ae5208d103
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts
@@ -0,0 +1,141 @@
+///
+/// Copyright © 2016-2025 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, forwardRef, Input, OnInit } from '@angular/core';
+import {
+ ControlValueAccessor,
+ FormBuilder,
+ FormGroup,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator,
+ ValidatorFn,
+ Validators
+} from '@angular/forms';
+import { EntityKeyValueType } from '@shared/models/query/query.models';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+import { AlarmRuleValue } from "@shared/models/alarm-rule.models";
+import { FormControlsFrom } from "@shared/models/tenant.model";
+
+@Component({
+ selector: 'tb-alarm-rule-filter-predicate-value',
+ templateUrl: './alarm-rule-filter-predicate-value.component.html',
+ styleUrls: [],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent),
+ multi: true
+ },
+ {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent),
+ multi: true
+ }
+ ]
+})
+export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit {
+
+ @Input()
+ arguments: Record;
+
+ @Input()
+ valueType: EntityKeyValueType;
+
+ valueTypeEnum = EntityKeyValueType;
+
+ filterPredicateValueFormGroup: FormGroup>>;
+
+ dynamicModeControl = this.fb.control(false);
+
+ argumentsList: Array;
+
+ private propagateChange= (v: any) => { };
+
+ constructor(private fb: FormBuilder,
+ private destroyRef: DestroyRef) {
+ }
+
+ ngOnInit(): void {
+ this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
+ let defaultValue: string | number | boolean;
+ let defaultValueValidators: ValidatorFn[];
+ switch (this.valueType) {
+ case EntityKeyValueType.STRING:
+ defaultValue = '';
+ defaultValueValidators = [];
+ break;
+ case EntityKeyValueType.NUMERIC:
+ defaultValue = 0;
+ defaultValueValidators = [Validators.required];
+ break;
+ case EntityKeyValueType.BOOLEAN:
+ defaultValue = false;
+ defaultValueValidators = [];
+ break;
+ case EntityKeyValueType.DATE_TIME:
+ defaultValue = Date.now();
+ defaultValueValidators = [Validators.required];
+ break;
+ }
+ this.filterPredicateValueFormGroup = this.fb.group({
+ staticValue: [defaultValue, defaultValueValidators],
+ dynamicValueArgument: ['', Validators.required]
+ });
+ this.filterPredicateValueFormGroup.valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe(() => {
+ this.updateModel();
+ });
+ this.dynamicModeControl.valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe(value => this.updateValueModeValidators(value))
+ }
+
+ private updateValueModeValidators(isDynamicMode: boolean): void {
+ if (isDynamicMode) {
+ this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false});
+ this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable({emitEvent: false});
+ } else {
+ this.filterPredicateValueFormGroup.get('staticValue').enable({emitEvent: false});
+ this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false});
+ }
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ validate(): ValidationErrors | null {
+ return this.filterPredicateValueFormGroup.valid ? null : {
+ filterPredicateValue: {valid: false}
+ };
+ }
+
+ writeValue(predicateValue: AlarmRuleValue): void {
+ this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false});
+ this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length);
+ }
+
+ private updateModel() {
+ this.propagateChange(this.filterPredicateValueFormGroup.value);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
new file mode 100644
index 0000000000..6e440eac86
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
@@ -0,0 +1,79 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
new file mode 100644
index 0000000000..a49d4cf42e
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
@@ -0,0 +1,146 @@
+///
+/// Copyright © 2016-2025 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, forwardRef, Input } from '@angular/core';
+import {
+ ControlValueAccessor,
+ FormBuilder,
+ NG_VALIDATORS,
+ NG_VALUE_ACCESSOR,
+ ValidationErrors,
+ Validator
+} from '@angular/forms';
+import {
+ BooleanOperation,
+ booleanOperationTranslationMap,
+ EntityKeyValueType,
+ FilterPredicateType,
+ NumericOperation,
+ numericOperationTranslationMap,
+ StringOperation,
+ stringOperationTranslationMap
+} from '@shared/models/query/query.models';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
+import { MatDialog } from "@angular/material/dialog";
+import {
+ AlarmRuleComplexFilterPredicateDialogComponent,
+ AlarmRuleComplexFilterPredicateDialogData
+} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component";
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+
+@Component({
+ selector: 'tb-alarm-rule-filter-predicate',
+ templateUrl: './alarm-rule-filter-predicate.component.html',
+ styleUrls: [],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent),
+ multi: true
+ },
+ {
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent),
+ multi: true
+ }
+ ]
+})
+export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, Validator {
+
+ @Input()
+ valueType: EntityKeyValueType;
+
+ @Input()
+ arguments: Record;
+
+ filterPredicateFormGroup = this.fb.group({
+ operation: [],
+ ignoreCase: false,
+ predicates: [],
+ value: []
+ });
+
+ type: FilterPredicateType;
+
+ filterPredicateType = FilterPredicateType;
+
+ stringOperations = Object.keys(StringOperation);
+ stringOperation = StringOperation;
+ stringOperationTranslationMap = stringOperationTranslationMap;
+
+ numericOperations = Object.keys(NumericOperation);
+ numericOperationEnum = NumericOperation;
+ numericOperationTranslations = numericOperationTranslationMap;
+
+ booleanOperations = Object.keys(BooleanOperation);
+ booleanOperationEnum = BooleanOperation;
+ booleanOperationTranslations = booleanOperationTranslationMap;
+
+ private propagateChange= (v: any) => { };
+
+ constructor(private fb: FormBuilder,
+ private dialog: MatDialog,
+ private destroyRef: DestroyRef) {
+ this.filterPredicateFormGroup.valueChanges.pipe(
+ takeUntilDestroyed(this.destroyRef)
+ ).subscribe(() => {
+ this.updateModel();
+ });
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ validate(): ValidationErrors | null {
+ return this.filterPredicateFormGroup.valid ? null : {
+ filterPredicate: {valid: false}
+ };
+ }
+
+ writeValue(predicate: AlarmRuleFilterPredicate): void {
+ this.type = predicate.type;
+ this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false});
+ }
+
+ private updateModel() {
+ this.propagateChange({type: this.type, ...this.filterPredicateFormGroup.value});
+ }
+
+ public openComplexFilterDialog() {
+ this.dialog.open(AlarmRuleComplexFilterPredicateDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ complexPredicate: this.filterPredicateFormGroup.value as ComplexAlarmRuleFilterPredicate,
+ valueType: this.valueType,
+ isAdd: false,
+ arguments: this.arguments,
+ }
+ }).afterClosed().subscribe(
+ (result) => {
+ if (result) {
+ this.filterPredicateFormGroup.patchValue(result);
+ }
+ }
+ );
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html
new file mode 100644
index 0000000000..cb390b6c0b
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html
@@ -0,0 +1,23 @@
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss
new file mode 100644
index 0000000000..a059bb4564
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss
@@ -0,0 +1,85 @@
+/**
+ * Copyright © 2016-2025 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 {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ .tb-filter-text {
+ overflow-y: auto;
+ text-align: start;
+ &.required {
+ color: #f44336;
+ padding: 0;
+ }
+ &.nowrap {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+}
+
+:host ::ng-deep {
+ .tb-filter-text {
+ line-height: 1.8em;
+ span {
+ display: inline-block;
+ vertical-align: middle;
+ line-height: 1.4em;
+ }
+ .tb-filter-predicate {
+ padding-right: 4px;
+ padding-left: 4px;
+ }
+ .tb-filter-entity-key, .tb-filter-value, .tb-filter-dynamic-source {
+ font-weight: bold;
+ border: 1px groove rgba(0, 0, 0, .25);
+ border-radius: 4px;
+ padding-left: 4px;
+ padding-right: 4px;
+ }
+ .tb-filter-entity-key, .tb-filter-value {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 150px;
+ }
+ .tb-filter-dynamic-source {
+ }
+ .tb-filter-entity-key {
+ color: #305680;
+ }
+ .tb-filter-value {
+ color: #ff5722;
+ }
+ .tb-filter-simple-operation {
+ font-size: 0.9em;
+ }
+ .tb-filter-complex-operation {
+ font-weight: 400;
+ font-style: italic;
+ }
+ .tb-filter-dynamic-value {
+ .tb-filter-dynamic-source, .tb-filter-value {
+ color: #0c959c;
+ }
+ }
+ .tb-filter-bracket {
+ .tb-left-bracket, .tb-right-bracket {
+ font-size: 1.2em;
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts
new file mode 100644
index 0000000000..498b2a0a81
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts
@@ -0,0 +1,192 @@
+///
+/// Copyright © 2016-2025 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, Input } from '@angular/core';
+import {
+ booleanOperationTranslationMap,
+ ComplexOperation,
+ complexOperationTranslationMap,
+ EntityKeyValueType,
+ FilterPredicateType,
+ numericOperationTranslationMap,
+ stringOperationTranslationMap
+} from '@shared/models/query/query.models';
+import { TranslateService } from '@ngx-translate/core';
+import { DatePipe } from '@angular/common';
+import {
+ AlarmRuleExpression,
+ AlarmRuleExpressionType,
+ AlarmRuleFilter,
+ AlarmRuleFilterPredicate,
+ ComplexAlarmRuleFilterPredicate
+} from "@shared/models/alarm-rule.models";
+import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
+import { coerceBoolean } from "@shared/decorators/coercion";
+
+@Component({
+ selector: 'tb-alarm-rule-filter-text',
+ templateUrl: './alarm-rule-filter-text.component.html',
+ styleUrls: ['./alarm-rule-filter-text.component.scss'],
+ providers: []
+})
+export class AlarmRuleFilterTextComponent {
+
+ @Input()
+ @coerceBoolean()
+ required = false;
+
+ @Input()
+ noFilterText = this.translate.instant('filter.no-filter-text');
+
+ @Input()
+ addFilterPrompt = this.translate.instant('filter.add-filter-prompt');
+
+ @Input()
+ @coerceBoolean()
+ nowrap = false;
+
+ @Input()
+ arguments: Record;
+
+ private alarmRuleExpressionValue: AlarmRuleExpression;
+ get alarmRuleExpression(): AlarmRuleExpression {
+ return this.alarmRuleExpressionValue;
+ }
+
+ @Input()
+ set alarmRuleExpression(value: AlarmRuleExpression) {
+ if (value !== this.alarmRuleExpressionValue) {
+ this.alarmRuleExpressionValue = value;
+ this.updateFilterText(value);
+ }
+ };
+
+ private specTextValue: string;
+ get specText(): string {
+ return this.specTextValue;
+ }
+ @Input()
+ set specText(value: string) {
+ if (value !== this.specTextValue) {
+ this.specTextValue = value;
+ this.updateFilterText(this.alarmRuleExpression);
+ }
+ }
+
+ isRequired = false;
+
+ public filterText: string;
+
+ constructor(private translate: TranslateService,
+ private datePipe: DatePipe) {
+ }
+
+ private updateFilterText(value: AlarmRuleExpression) {
+ this.isRequired = false;
+ if (value && (value.expression || value.filters)) {
+ if (value.type === AlarmRuleExpressionType.SIMPLE) {
+ this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation);
+ } else {
+ this.filterText = 'function expression(ctx, ' + (this.arguments ? Object.keys(this.arguments).join(', ') : '' ) + ')';
+ }
+ if (this.specText?.length) {
+ this.filterText = this.specText + ': ' + this.filterText;
+ }
+ } else {
+ if (this.required) {
+ this.filterText = this.addFilterPrompt;
+ this.isRequired = true;
+ } else {
+ this.filterText = this.noFilterText;
+ }
+ }
+ }
+
+ private keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array, operation: ComplexOperation): string {
+ const filtersText = keyFilters.map(keyFilter =>
+ this.filterPredicateToText(translate, datePipe, keyFilter, keyFilter.predicates));
+ let result: string;
+ if (filtersText.length > 1) {
+ const operationText = translate.instant(complexOperationTranslationMap.get(operation));
+ result = filtersText.join(' ' + operationText + ' ');
+ } else {
+ result = filtersText[0];
+ }
+ return result;
+ }
+
+ private filterPredicateToText(translate: TranslateService,
+ datePipe: DatePipe,
+ keyFilter: AlarmRuleFilter,
+ keyFilterPredicates: AlarmRuleFilterPredicate[],
+ complexOperation?: ComplexOperation): string {
+ const key = keyFilter.argument;
+ const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND);
+
+ const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => {
+ if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) {
+ const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate;
+ const complexOperation = complexPredicate.operation ?? ComplexOperation.AND;
+ return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation);
+ } else {
+ let operation: string;
+ let value: string;
+ const val = keyFilterPredicate.value;
+ const dynamicValue = val?.dynamicValueArgument?.length;
+ if (dynamicValue) {
+ value = '' + val?.dynamicValueArgument + '';
+ }
+ switch (keyFilterPredicate.type) {
+ case FilterPredicateType.STRING:
+ operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation));
+ if (keyFilterPredicate.ignoreCase) {
+ operation += ' ' + translate.instant('filter.ignore-case');
+ }
+ if (!dynamicValue) {
+ value = `'${keyFilterPredicate.value.staticValue}'`;
+ }
+ break;
+ case FilterPredicateType.NUMERIC:
+ operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation));
+ if (!dynamicValue) {
+ if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) {
+ value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm');
+ } else {
+ value = keyFilterPredicate.value.staticValue + '';
+ }
+ }
+ break;
+ case FilterPredicateType.BOOLEAN:
+ operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation));
+ if (!dynamicValue) {
+ value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false');
+ }
+ break;
+ }
+ if (!dynamicValue) {
+ value = `${value}`;
+ }
+ return `${key} ${operation} ${value}`
+ }
+ });
+ if (predicates.length > 1) {
+ return '(' + predicates.join(` ${translate.instant(complexOperationTranslationMap.get(filterOperation))} `)+ ')';
+ } else {
+ return predicates.toString();
+ }
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
index 1b48771247..8cc5d10fc6 100644
--- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
+++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
@@ -77,6 +77,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('createdTime', 'common.created-time', this.datePipe, '150px'));
this.columns.push(new EntityTableColumn('name', 'common.name', '33%'));
- this.columns.push(new EntityTableColumn