diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
index f06d4cec53..9bab4060ab 100644
--- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
+++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
@@ -26,7 +26,6 @@ import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
-import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@@ -58,15 +57,16 @@ public class TbDate implements Serializable, Cloneable {
this.instant = parseInstant(s);
}
- public TbDate(String s, String pattern, Locale locale) {
- instant = parseInstant(s, pattern, locale, ZoneId.systemDefault());
+ public TbDate(String s, String pattern) {
+ this.instant = parseInstant(s, Locale.getDefault().toLanguageTag(), pattern);
}
- public TbDate(String s, String pattern, Locale locale, String zoneIdStr) {
- ZoneId zoneId = ZoneId.of(zoneIdStr);
- instant = parseInstant(s, pattern, locale, zoneId);
+
+ public TbDate(String s, String pattern, String locale) {
+ this.instant = parseInstant(s, locale, pattern);
}
- public TbDate(String s, String pattern, Locale locale, ZoneId zoneId) {
- instant = parseInstant(s, pattern, locale, zoneId);
+
+ public TbDate(String s, String pattern, String locale, String zoneId) {
+ this.instant = parseInstant(s, pattern, locale, zoneId);
}
public TbDate(long dateMilliSecond) {
@@ -488,18 +488,25 @@ public class TbDate implements Serializable, Cloneable {
}
private static Instant parseInstant(String s) {
- try{
- if (s.length() > 0 && Character.isDigit(s.charAt(0))) {
- // assuming UTC instant "2007-12-03T10:15:30.00Z"
- return OffsetDateTime.parse(s).toInstant();
- }
- else {
- // assuming RFC-1123 value "Tue, 3 Jun 2008 11:05:30 GMT-02:00"
- return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(s));
+ boolean isIsoFormat = s.length() > 0 && Character.isDigit(s.charAt(0));
+ if (isIsoFormat) {
+ return getInstant_ISO_OFFSET_DATE_TIME(s);
+ } else {
+ return getInstant_RFC_1123(s);
+ }
+ }
+
+ private static Instant parseInstant(String s, String localeStr, String pattern) {
+ try {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, Locale.forLanguageTag(localeStr));
+ return Instant.from(formatter.parse(s));
+ } catch (Exception ex) {
+ try {
+ return parseInstant(s, pattern, localeStr, ZoneId.systemDefault().getId());
+ } catch (final DateTimeParseException e) {
+ final ConversionException exception = new ConversionException("Cannot parse value [" + s + "] as instant", ex);
+ throw exception;
}
- } catch (final DateTimeParseException ex) {
- final ConversionException exception = new ConversionException("Cannot parse value [" + s + "] as instant", ex);
- throw exception;
}
}
@@ -508,10 +515,55 @@ public class TbDate implements Serializable, Cloneable {
ZonedDateTime zonedDateTime = ZonedDateTime.of(year, month, date, hrs, min, second, secondMilli*1000000, zoneId);
return zonedDateTime.toInstant();
}
- private static Instant parseInstant(String s, String pattern, Locale locale, ZoneId zoneId) {
- DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern, locale);
+ private static Instant parseInstant(String s, String pattern, String localeStr, String zoneIdStr) {
+ DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.forLanguageTag(localeStr));
LocalDateTime localDateTime = LocalDateTime.parse(s, dateTimeFormatter);
- ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
+ ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of(zoneIdStr));
return zonedDateTime.toInstant();
}
+
+ private static Instant getInstant_ISO_OFFSET_DATE_TIME(String s) {
+ // assuming "2007-12-03T10:15:30.00Z" UTC instant
+ // assuming "2007-12-03T10:15:30.00" ZoneId.systemDefault() instant
+ // assuming "2007-12-03T10:15:30.00-04:00" TZ instant
+ // assuming "2007-12-03T10:15:30.00+04:00" TZ instant
+ DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+ try {
+ return Instant.from(formatter.parse(s));
+ } catch (DateTimeParseException ex) {
+ try {
+ long timeMS = parse(s);
+ if (timeMS != -1) {
+ return Instant.ofEpochMilli(timeMS);
+ } else {
+ throw new ConversionException("Cannot parse value [" + s + "] as instant");
+ }
+ } catch (final DateTimeParseException e) {
+ throw new ConversionException("Cannot parse value [" + s + "] as instant");
+ }
+ }
+ }
+ private static Instant getInstant_RFC_1123(String s) {
+ // assuming RFC-1123 value "Tue, 3 Jun 2008 11:05:30 GMT"
+ // assuming RFC-1123 value "Tue, 3 Jun 2008 11:05:30 GMT-02:00"
+ // assuming RFC-1123 value "Tue, 3 Jun 2008 11:05:30 -0200"
+ DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME;
+ try {
+ return Instant.from(formatter.parse(s));
+ } catch (DateTimeParseException ex) {
+ try {
+ return getInstantWithLocalZoneOffsetId_RFC_1123(s);
+ } catch (final DateTimeParseException e) {
+ throw new ConversionException("Cannot parse value [" + s + "] as instant");
+ }
+ }
+ }
+ private static Instant getInstantWithLocalZoneOffsetId_RFC_1123(String value) {
+ String s = value.trim() + " GMT";
+ Instant instant = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(s));
+ ZoneId systemZone = ZoneId.systemDefault(); // my timezone
+ String id = systemZone.getRules().getOffset(instant).getId();
+ value = value.trim() + " " + id.replaceAll(":", "");
+ return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(value));
+ }
}
diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateConstructorTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateConstructorTest.java
new file mode 100644
index 0000000000..8a9d84b980
--- /dev/null
+++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateConstructorTest.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.script.api.tbel;
+
+import org.junit.Assert;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mvel2.CompileException;
+import org.mvel2.ExecutionContext;
+import org.mvel2.ParserContext;
+import org.mvel2.SandboxedParserConfiguration;
+
+import java.io.Serializable;
+import java.util.HashMap;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mvel2.MVEL.compileExpression;
+import static org.mvel2.MVEL.executeTbExpression;
+
+public class TbDateConstructorTest {
+
+ private static ExecutionContext executionContext;
+
+ @BeforeAll
+ public static void setup() {
+ SandboxedParserConfiguration parserConfig = ParserContext.enableSandboxedMode();
+ parserConfig.addImport("JSON", TbJson.class);
+ parserConfig.registerDataType("Date", TbDate.class, date -> 8L);
+ executionContext = new ExecutionContext(parserConfig, 5 * 1024 * 1024);
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ ParserContext.disableSandboxedMode();
+ }
+
+
+ @Test
+ void TestTbDateConstructorWithStringParameters () {
+ // one: date in String
+ String body = "var d = new Date(\"2023-08-06T04:04:05.123Z\"); \n" +
+ "d.toISOString()";
+ Object res = executeScript(body);
+ Assert.assertNotEquals("2023-08-06T04:04:05.123Z".length(), res);
+
+ // two: date in String + pattern
+ body = "var pattern = \"yyyy-MM-dd HH:mm:ss.SSSXXX\";\n" +
+ "var d = new Date(\"2023-08-06 04:04:05.000Z\", pattern);\n" +
+ "d.toISOString()";
+ res = executeScript(body);
+ Assert.assertNotEquals("2023-08-06T04:04:05Z".length(), res);
+
+
+ // three: date in String + pattern + locale
+ body = "var pattern = \"hh:mm:ss a, EEE M/d/uuuu\";\n" +
+ "var d = new Date(\"02:15:30 PM, Sun 10/09/2022\", pattern, \"en-US\");" +
+ "d.toISOString()";
+ res = executeScript(body);
+ Assert.assertNotEquals("2023-08-06T04:04:05Z".length(), res);
+
+ // four: date in String + pattern + locale + TimeZone
+ body = "var pattern = \"hh:mm:ss a, EEE M/d/uuuu\";\n" +
+ "var d = new Date(\"02:15:30 PM, Sun 10/09/2022\", pattern, \"en-US\", \"America/New_York\");" +
+ "d.toISOString()";
+ res = executeScript(body);
+ Assert.assertNotEquals("22022-10-09T18:15:30Z".length(), res);
+ }
+
+ @Test
+ void TbDateConstructorWithStringParameters_PatternNotMatchLocale_Error () {
+ String expectedMessage = "could not create constructor: null";
+
+ String body = "var pattern = \"hh:mm:ss a, EEE M/d/uuuu\";\n" +
+ "var d = new Date(\"02:15:30 PM, Sun 10/09/2022\", pattern, \"de\");" +
+ "d.toISOString()";
+ Exception actual = assertThrows(CompileException.class, () -> {
+ executeScript(body);
+ });
+ assertTrue(actual.getMessage().contains(expectedMessage));
+
+ }
+
+ private Object executeScript(String ex) {
+ Serializable compiled = compileExpression(ex, new ParserContext());
+ return executeTbExpression(compiled, executionContext, new HashMap());
+ }
+}
diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java
index ab0b2875bd..120a776c1a 100644
--- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java
+++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java
@@ -15,8 +15,6 @@
*/
package org.thingsboard.script.api.tbel;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -36,7 +34,6 @@ import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@@ -156,7 +153,7 @@ class TbDateTest {
void testToLocaleDateString() {
String s = "02:15:30 PM, Sun 10/09/2022";
String pattern = "hh:mm:ss a, EEE M/d/uuuu";
- TbDate d = new TbDate(s, pattern, Locale.US);
+ TbDate d = new TbDate(s, pattern, "en-US");
// tz = local
int localOffsetHrs = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds()/60/60;
int hrs = 14 - localOffsetHrs;
@@ -169,9 +166,9 @@ class TbDateTest {
Assert.assertEquals("2023-08-06T08:04:05Z", d.toISOString());
s = "02:15:30 PM, Sun 10/09/2022";
- d = new TbDate(s, pattern, Locale.US, "-04:00");
+ d = new TbDate(s, pattern, "en-US", "-04:00");
Assert.assertEquals("2022-10-09T18:15:30Z", d.toISOString());
- d = new TbDate(s, pattern, Locale.US, "America/New_York");
+ d = new TbDate(s, pattern,"en-US", "America/New_York");
Assert.assertEquals("2022-10-09T18:15:30Z", d.toISOString());
// tz = "+02:00"
/**
@@ -181,12 +178,12 @@ class TbDateTest {
* `{ "AM", "PM" }`
*/
s = "09:15:30 nachm., So. 10/09/2022";
- d = new TbDate(s, pattern, Locale.GERMAN, ZoneId.of("Europe/Berlin"));
+ d = new TbDate(s, pattern, "de","Europe/Berlin");
Assert.assertEquals("2022-10-09T19:15:30Z", d.toISOString());
s = "02:15:30 пп, середа, 4 жовтня 2023 р.";
pattern = "hh:mm:ss a, EEEE, d MMMM y 'р.'";
- d = new TbDate(s, pattern, Locale.forLanguageTag("uk-UA"));
+ d = new TbDate(s, pattern, "uk-UA");
localOffsetHrs = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds()/60/60;
hrs = 14 - localOffsetHrs;
expected = "2023-10-04T" + hrs + ":15:30Z";
@@ -353,13 +350,19 @@ class TbDateTest {
stringDateTZ = "2023-09-06T01:04:05.00-04:00";
d = new TbDate(stringDateTZ);
Assert.assertEquals("2023-09-06T05:04:05Z", d.toISOString());
- stringDateTZ = "2023-09-06T01:04:05.00+04:30:56";
+ stringDateTZ = "2023-09-06T01:04:05.00+04:30:56";
d = new TbDate(stringDateTZ);
Assert.assertEquals("2023-09-05T20:33:09Z", d.toISOString());
stringDateTZ = "2023-09-06T01:04:05.00-02:00";
d = new TbDate(stringDateTZ);
Assert.assertEquals("2023-09-06T03:04:05Z", d.toISOString());
- String stringDateRFC_1123 = "Sat, 3 Jun 2023 11:05:30 GMT";
+ // Without_TZ
+ stringDateTZ = "2023-08-06T04:04:05.123";
+ d = new TbDate(stringDateTZ);
+ Assert.assertEquals("2023-08-06 04:04:05", d.toLocaleString());
+
+ // With TZ RFC_1123
+ String stringDateRFC_1123 = "Sat, 3 Jun 2023 11:05:30 GMT";
d = new TbDate(stringDateRFC_1123);
Assert.assertEquals("2023-06-03T11:05:30Z", d.toISOString());
stringDateRFC_1123 = "Sat, 3 Jun 2023 01:04:05 +043056";
@@ -371,28 +374,57 @@ class TbDateTest {
stringDateRFC_1123 = "Thu, 29 Feb 2024 11:05:30 -03";
d = new TbDate(stringDateRFC_1123);
Assert.assertEquals("2024-02-29T14:05:30Z", d.toISOString());
+ // Without TZ RFC_1123
+ stringDateRFC_1123 = "Sat, 3 Jun 2023 11:05:30";
+ d = new TbDate(stringDateRFC_1123);
+ Assert.assertEquals("2023-06-03 11:05:30", d.toLocaleString());
+
+ // With pattern + locale - ok
+ String pattern = "hh:mm:ss a, EEE M/d/uuuu";
+ stringDateRFC_1123 = "09:15:30 nachm., So. 10/09/2022";
+ d = new TbDate(stringDateRFC_1123 , pattern, "de");
+ Assert.assertEquals("2022-10-09 21:15:30", d.toLocaleString());
- String stringDateZ_error = "2023-09-06T01:04:05.00+045";
+ // failed TZ
+ String expectedMessage = "Cannot parse value";
+ String finalStringDateZ_error0 = "2023-09-06T01:04:05.00+045";
Exception actual = assertThrows(ConversionException.class, () -> {
- new TbDate(stringDateZ_error);
+ new TbDate(finalStringDateZ_error0);
+ });
+ assertTrue(actual.getMessage().contains(expectedMessage));
+ // failed TZ
+ String finalStringDateZ_error1 = "2023-08-06T04:04:05.123+04:00:00:00";
+ actual = assertThrows(ConversionException.class, () -> {
+ new TbDate(finalStringDateZ_error1);
+ });
+ assertTrue(actual.getMessage().contains(expectedMessage));
+ // failed TZ
+ String finalStringDateZ_error2 ="2023-08-06T04:04:05.123+4";
+ actual = assertThrows(ConversionException.class, () -> {
+ new TbDate(finalStringDateZ_error2);
+ });
+ assertTrue(actual.getMessage().contains(expectedMessage));
+ // The locale does not match the pattern RFC_1123
+ String finalStringDateZ_error3= "02:15:30 PM, Sun 10/09/2022";
+ pattern = "hh:mm:ss a, EEE M/d/uuuu";
+ String finalPattern = pattern;
+ actual = assertThrows(ConversionException.class, () -> {
+ new TbDate(finalStringDateZ_error3, finalPattern, "de");
});
- String expectedMessage = "Cannot parse value";
assertTrue(actual.getMessage().contains(expectedMessage));
+ // failed DayOfWeek RFC_1123
String stringDateRFC_1123_error = "Tue, 3 Jun 2023 11:05:30 GMT";
actual = assertThrows(ConversionException.class, () -> {
new TbDate(stringDateRFC_1123_error);
});
assertTrue(actual.getMessage().contains(expectedMessage));
}
+
@Test
void TestParse () {
- String stringDateUTC = "2023-09-06T01:04:05.345Z";
- TbDate d = new TbDate(stringDateUTC);
- Assert.assertEquals(1693962245345L, d.parseSecondMilli());
- Assert.assertEquals(1693962245L, d.parseSecond());
String stringDateStart = "1970-01-01T00:00:00Z";
- d = new TbDate(stringDateStart);
+ TbDate d = new TbDate(stringDateStart);
long actualMillis = TbDate.parse("1970-01-01 T00:00:00");
Assert.assertEquals(-d.getLocaleZoneOffset().getTotalSeconds() * 1000, actualMillis);
String pattern = "yyyy-MM-dd HH:mm:ss.SSS";
@@ -454,6 +486,7 @@ class TbDateTest {
@Test
void Test_Year_Moth_Date_Hours_Min_Sec_Without_TZ() {
+
TbDate d = new TbDate(2023, 8, 18);
Assert.assertEquals("2023-08-18 00:00:00", d.toLocaleString());
d = new TbDate(2023, 9, 17, 17, 34);
@@ -466,6 +499,45 @@ class TbDateTest {
Assert.assertEquals("2023-09-07 08:04:05", d.toLocaleString());
}
+ @Test
+ void Test_DateString_With_Pattern() {
+ String pattern = "yyyy-MM-dd HH:mm:ss.SSSXXX";
+ TbDate d = new TbDate("2023-08-06 04:04:05.000-04:00", pattern);
+ Assert.assertEquals("2023-08-06T08:04:05Z", d.toISOString());
+ }
+ @Test
+ void Test_DateString_With_TZ() {
+ int date = 7;
+ int tz = -4;
+ int hrs = 8;
+ String pattern = "2023-09-%s %s:04:05";
+ String tzStr = "-04:00:00";
+ String dateStr = "2023-09-0" + date + "T0" + hrs + ":04:05.123" + tzStr;
+ TbDate d = new TbDate(dateStr);
+ int localOffsetHrs = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds()/60/60;
+ TbDateTestEntity tbDateTest = new TbDateTestEntity(23, 9, date, hrs + localOffsetHrs - tz);
+ String expected = String.format(pattern, tbDateTest.geDateStr(), tbDateTest.geHoursStr());
+ Assert.assertEquals(expected, d.toLocaleString());
+
+ tz = +5;
+ tzStr = "+05:00";
+ dateStr = "2023-09-0" + date + "T0" + hrs + ":04:05.123" + tzStr;
+ d = new TbDate(dateStr);
+ localOffsetHrs = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds()/60/60;
+ tbDateTest = new TbDateTestEntity(23, 9, date, hrs + localOffsetHrs - tz);
+ expected = String.format(pattern, tbDateTest.geDateStr(), tbDateTest.geHoursStr());
+ Assert.assertEquals(expected, d.toLocaleString());
+
+ tz = -2;
+ tzStr = "-02";
+ dateStr = "2023-09-0" + date + "T0" + hrs + ":04:05.123" + tzStr;
+ d = new TbDate(dateStr);
+ localOffsetHrs = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds()/60/60;
+ tbDateTest = new TbDateTestEntity(23, 9, date, hrs + localOffsetHrs - tz);
+ expected = String.format(pattern, tbDateTest.geDateStr(), tbDateTest.geHoursStr());
+ Assert.assertEquals(expected, d.toLocaleString());
+ }
+
@Test
void Test_Get_LocalDateTime_With_TZ() {
int hrs = 8;
diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.html b/ui-ngx/src/app/shared/components/image/image-gallery.component.html
index 7624152f33..69f4dcb943 100644
--- a/ui-ngx/src/app/shared/components/image/image-gallery.component.html
+++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.html
@@ -151,7 +151,9 @@
-
+
+
![{{ image.title }}]()
+
@@ -271,7 +273,8 @@
-
diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.scss b/ui-ngx/src/app/shared/components/image/image-gallery.component.scss
index 83dd278e4b..6272b0d42d 100644
--- a/ui-ngx/src/app/shared/components/image/image-gallery.component.scss
+++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.scss
@@ -275,10 +275,14 @@ $tb-button-selected-color: rgb(255, 110, 64) !default;
width: 50px;
height: 50px;
padding: 2px 12px;
- display: block;
}
}
+ .tb-image-preview-cell {
+ width: 50px;
+ height: 50px;
+ }
+
.tb-image-preview {
width: 50px;
height: 50px;
diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts
index 6efc629c33..05b1d2e638 100644
--- a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts
+++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts
@@ -29,7 +29,7 @@ import {
AfterViewInit,
ChangeDetectorRef,
Component,
- ElementRef, EventEmitter,
+ ElementRef, EventEmitter, HostBinding,
Input,
OnDestroy,
OnInit, Output,
@@ -108,6 +108,9 @@ const popoverGridColumns: ScrollGridColumns = {
})
export class ImageGalleryComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit {
+ @HostBinding('style.display')
+ private display = 'block';
+
@Input()
@coerceBoolean()
pageMode = true;