Browse Source

Merge branch 'master' into avoid-duplicate-edge-updates

pull/14489/head
Mazurenko Nikita 9 months ago
committed by GitHub
parent
commit
3a6718c3e2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      application/pom.xml
  2. 91
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  3. 27
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java
  5. 21
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
  6. 37
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  7. 2
      common/data/pom.xml
  8. 5
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
  9. 8
      pom.xml
  10. 2
      rule-engine/rule-engine-api/pom.xml
  11. 2
      rule-engine/rule-engine-components/pom.xml
  12. 5
      ui-ngx/package.json
  13. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  14. 4
      ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts
  15. 16
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts
  16. 10
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  17. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  18. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts
  19. 2
      ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts
  20. 30
      ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts
  21. 2
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html
  22. 31
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  23. 10
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html
  24. 13
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts
  25. 5
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  26. 4
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss
  27. 16
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  28. 20
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  29. 9
      ui-ngx/src/app/shared/models/ai-model.models.ts
  30. 7
      ui-ngx/src/app/shared/models/time/time.models.ts
  31. 12
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  32. 78
      ui-ngx/yarn.lock

18
application/pom.xml

@ -382,35 +382,35 @@
<artifactId>rocksdbjni</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-azure-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-google-ai-gemini</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-vertex-ai-gemini</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-mistral-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-bedrock</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-github-models</artifactId>
<exclusions>
<exclusion>
@ -420,7 +420,7 @@
</exclusions>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
</dependency>
</dependencies>

91
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -69,6 +69,7 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -82,6 +83,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME;
@ -94,6 +96,7 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS
public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService {
private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, EdgeGrpcSession> sessionsById = new ConcurrentHashMap<>();
private final ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>();
@ -283,6 +286,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
destroySession(session);
session.cleanUp();
sessions.remove(edgeId);
sessionsById.remove(session.getSessionId());
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
@ -332,9 +336,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
TenantId tenantId = edge.getTenantId();
log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId);
if (sessions.containsKey(edgeId)) {
destroySession(sessions.get(edgeId));
EdgeGrpcSession existing = sessions.get(edgeId);
if (existing != null) {
log.info("[{}][{}] Replacing existing session [{}] for edge [{}]", tenantId, edgeGrpcSession.getSessionId(), existing.getSessionId(), edgeId);
destroySession(existing);
sessionsById.remove(existing.getSessionId());
}
}
sessions.put(edgeId, edgeGrpcSession);
sessionsById.put(edgeGrpcSession.getSessionId(), edgeGrpcSession);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
@ -492,9 +502,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void onEdgeDisconnect(Edge edge, UUID sessionId) {
EdgeId edgeId = edge.getId();
log.info("[{}][{}] edge disconnected!", edgeId, sessionId);
EdgeGrpcSession toRemove = sessions.get(edgeId);
if (toRemove.getSessionId().equals(sessionId)) {
toRemove = sessions.remove(edgeId);
EdgeGrpcSession current = sessions.get(edgeId);
if (current != null && current.getSessionId().equals(sessionId)) {
EdgeGrpcSession toRemove = sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
@ -503,6 +513,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
newEventLock.unlock();
}
destroySession(toRemove);
sessionsById.remove(sessionId);
TenantId tenantId = toRemove.getEdge().getTenantId();
save(tenantId, edgeId, ACTIVITY_STATE, false);
long lastDisconnectTs = System.currentTimeMillis();
@ -510,7 +521,18 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId);
} else {
log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId);
log.info("[{}] edge session [{}] is not current anymore. Attempting to destroy it by sessionId.", edgeId, sessionId);
EdgeGrpcSession stale = sessionsById.remove(sessionId);
if (stale != null) {
try {
destroySession(stale);
log.info("[{}][{}] Successfully destroyed stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId);
} catch (Exception e) {
log.warn("[{}][{}] Failed to destroy stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId, e);
}
} else {
log.debug("[{}] No session found by sessionId [{}] to destroy", edgeId, sessionId);
}
}
edgeIdServiceIdCache.evict(edgeId);
}
@ -522,6 +544,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId());
zombieSessions.add(session);
}
} catch (Exception e) {
log.warn("[{}][{}] Exception during session destroy for edge [{}] with session id [{}]",
session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId(), e);
}
}
@ -631,25 +656,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void cleanupZombieSessions() {
try {
List<EdgeId> toRemove = new ArrayList<>();
for (EdgeGrpcSession session : sessions.values()) {
if (session instanceof KafkaEdgeGrpcSession kafkaSession &&
!kafkaSession.isConnected() &&
kafkaSession.getConsumer() != null &&
kafkaSession.getConsumer().getConsumer() != null &&
!kafkaSession.getConsumer().getConsumer().isStopped()) {
toRemove.add(kafkaSession.getEdge().getId());
}
}
for (EdgeId edgeId : toRemove) {
log.info("[{}] Destroying session for edge because edge is not connected", edgeId);
EdgeGrpcSession removed = sessions.get(edgeId);
if (removed instanceof KafkaEdgeGrpcSession kafkaSession) {
if (kafkaSession.destroy()) {
sessions.remove(edgeId);
}
}
}
tryToDestroyZombieSessions(getZombieSessions(sessions.values()), s -> sessions.remove(s.getEdge().getId()));
tryToDestroyZombieSessions(getZombieSessions(sessionsById.values()), s -> sessionsById.remove(s.getSessionId()));
zombieSessions.removeIf(zombie -> {
if (zombie.destroy()) {
log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].",
@ -665,4 +674,38 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
log.warn("Failed to cleanup kafka sessions", e);
}
}
private List<EdgeGrpcSession> getZombieSessions(Collection<EdgeGrpcSession> sessions) {
List<EdgeGrpcSession> result = new ArrayList<>();
for (EdgeGrpcSession session : sessions) {
if (isKafkaSessionAndZombie(session)) {
result.add(session);
}
}
return result;
}
private void tryToDestroyZombieSessions(List<EdgeGrpcSession> sessionsToRemove, Function<EdgeGrpcSession, EdgeGrpcSession> removeFunc) {
for (EdgeGrpcSession toRemove : sessionsToRemove) {
log.info("[{}] Destroying session for edge because edge is not connected", toRemove.getEdge().getId());
if (toRemove.destroy()) {
removeFunc.apply(toRemove);
}
}
}
private boolean isKafkaSessionAndZombie(EdgeGrpcSession session) {
if (session instanceof KafkaEdgeGrpcSession kafkaSession) {
log.debug("[{}] kafkaSession.isConnected() = {}, kafkaSession.getConsumer().getConsumer().isStopped() = {}",
kafkaSession.getEdge().getId(),
kafkaSession.isConnected(),
kafkaSession.getConsumer() != null ? kafkaSession.getConsumer().getConsumer() != null ? kafkaSession.getConsumer().getConsumer().isStopped() : null : null);
return !kafkaSession.isConnected() &&
kafkaSession.getConsumer() != null &&
kafkaSession.getConsumer().getConsumer() != null &&
!kafkaSession.getConsumer().getConsumer().isStopped();
}
return false;
}
}

27
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java

@ -101,8 +101,21 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
@Override
public ListenableFuture<Boolean> processEdgeEvents() {
if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) {
log.warn("[{}][{}] Session is not ready (connected={}, syncInProgress={}, highPriority={}), skip starting edge event consumer",
tenantId, edge != null ? edge.getId() : null, isConnected(), isSyncInProgress(), isHighPriorityProcessing);
return Futures.immediateFuture(Boolean.FALSE);
}
if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) {
try {
if (consumerExecutor != null && !consumerExecutor.isShutdown()) {
try {
consumerExecutor.shutdown();
awaitConsumerTermination();
} catch (Exception e) {
log.warn("[{}][{}] Failed to shutdown previous consumer executor", tenantId, edge.getId(), e);
}
}
this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer"));
this.consumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdgeEventNotificationMsg>>builder()
.name("TB Edge events [" + edge.getId() + "]")
@ -133,6 +146,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
public boolean destroy() {
try {
if (consumer != null) {
log.info("[{}][{}] Stopping edge event consumer...", tenantId, edge != null ? edge.getId() : null);
consumer.stop();
}
} catch (Exception e) {
@ -141,16 +155,25 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
}
consumer = null;
try {
if (consumerExecutor != null) {
if (consumerExecutor != null && !consumerExecutor.isShutdown()) {
consumerExecutor.shutdown();
awaitConsumerTermination();
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to shutdown consumer executor", tenantId, edge.getId(), e);
log.warn("[{}][{}] Failed to shutdown edge event consumer executor", tenantId, edge.getId(), e);
return false;
}
return true;
}
private void awaitConsumerTermination() {
try {
consumerExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
} catch (InterruptedException ie) {
log.warn("[{}][{}] Interrupted while awaiting consumer executor termination", tenantId, edge.getId());
}
}
@Override
public void cleanUp() {
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic();

6
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java

@ -68,7 +68,7 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
return created;
}
private void updateDashboardAssignments(TenantId tenantId, Dashboard dashboardById, Dashboard savedDashboard, Set<ShortCustomerInfo> newAssignedCustomers) {
private void updateDashboardAssignments(TenantId tenantId, CustomerId edgeCustomerId, Dashboard dashboardById, Dashboard savedDashboard, Set<ShortCustomerInfo> newAssignedCustomers) {
Set<ShortCustomerInfo> currentAssignedCustomers = new HashSet<>();
if (dashboardById != null) {
if (dashboardById.getAssignedCustomers() != null) {
@ -76,7 +76,7 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
}
}
newAssignedCustomers = filterNonExistingCustomers(tenantId, currentAssignedCustomers, newAssignedCustomers);
newAssignedCustomers = filterNonExistingCustomers(tenantId, edgeCustomerId, currentAssignedCustomers, newAssignedCustomers);
Set<CustomerId> addedCustomerIds = new HashSet<>();
Set<CustomerId> removedCustomerIds = new HashSet<>();
@ -112,6 +112,6 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
}
}
protected abstract Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers);
protected abstract Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, CustomerId customerId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers);
}

21
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
@ -36,8 +37,10 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Component
@ -116,14 +119,24 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da
}
@Override
protected Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers) {
newAssignedCustomers.addAll(currentAssignedCustomers);
return newAssignedCustomers;
protected Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, CustomerId edgeCustomerId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers) {
boolean edgeCustomerPresentInNewAssignments = newAssignedCustomers.stream()
.map(ShortCustomerInfo::getCustomerId)
.anyMatch(edgeCustomerId::equals);
if (edgeCustomerPresentInNewAssignments) {
Set<ShortCustomerInfo> result = new HashSet<>(newAssignedCustomers);
result.addAll(currentAssignedCustomers);
return result;
} else {
return currentAssignedCustomers.stream()
.filter(info -> !edgeCustomerId.equals(info.getCustomerId()))
.collect(Collectors.toSet());
}
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.DASHBOARD;
}
}

37
application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java

@ -27,11 +27,15 @@ import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeConfiguration;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
@ -183,6 +187,22 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
customer.setTitle("Edge Customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
// assign edge to customer
edgeImitator.expectMessageAmount(2);
doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<EdgeConfiguration> edgeConfigurationOpt = edgeImitator.findMessageByType(EdgeConfiguration.class);
Assert.assertTrue(edgeConfigurationOpt.isPresent());
EdgeConfiguration edgeConfiguration = edgeConfigurationOpt.get();
Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), edgeConfiguration.getCustomerIdMSB());
Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), edgeConfiguration.getCustomerIdLSB());
Optional<CustomerUpdateMsg> customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class);
Assert.assertTrue(customerUpdateOpt.isPresent());
CustomerUpdateMsg customerUpdateMsg = customerUpdateOpt.get();
Customer customerMsg = JacksonUtil.fromString(customerUpdateMsg.getEntity(), Customer.class, true);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomer, customerMsg);
Dashboard dashboard = buildDashboardForUplinkMsg(savedCustomer);
// create dashboard on edge
@ -225,6 +245,23 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class);
Assert.assertEquals(DASHBOARD_TITLE + " Updated", foundDashboard.getName());
// unassign edge from customer
edgeImitator.expectMessageAmount(2);
doDelete("/api/customer/edge/" + edge.getUuidId(), Edge.class);
Assert.assertTrue(edgeImitator.waitForMessages());
edgeConfigurationOpt = edgeImitator.findMessageByType(EdgeConfiguration.class);
Assert.assertTrue(edgeConfigurationOpt.isPresent());
edgeConfiguration = edgeConfigurationOpt.get();
Assert.assertEquals(
new CustomerId(EntityId.NULL_UUID),
new CustomerId(new UUID(edgeConfiguration.getCustomerIdMSB(), edgeConfiguration.getCustomerIdLSB())));
customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class);
Assert.assertTrue(customerUpdateOpt.isPresent());
customerUpdateMsg = customerUpdateOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, customerUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), customerUpdateMsg.getIdMSB());
Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), customerUpdateMsg.getIdLSB());
}
@Test

2
common/data/pom.xml

@ -113,7 +113,7 @@
<scope>compile</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
</dependency>
</dependencies>

5
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java

@ -105,6 +105,7 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
public static final String CLAIMING = "claiming";
public static final String ATTRIBUTE = "attribute";
public static final String RPC_RESPONSE = "Rpc response";
public static final String CONNECT = "connect";
public static final String ATTRIBUTES_REQUEST = "attributes request";
protected final MqttTransportContext context;
@ -257,12 +258,14 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
protected void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) {
log.trace("[{}][{}][{}] onDeviceConnect: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName);
int msgId = getMsgId(msg);
AtomicBoolean ackSent = new AtomicBoolean(false);
process(onDeviceConnect(deviceName, deviceType),
result -> {
ack(msg, MqttReasonCodes.PubAck.SUCCESS);
log.trace("[{}][{}][{}] onDeviceConnectOk: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName);
},
t -> logDeviceCreationError(t, deviceName));
t -> processFailure(msgId, deviceName, CONNECT, ackSent, t));
}
public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional<DeviceProfile> deviceProfileOpt) {

8
pom.xml

@ -110,7 +110,7 @@
<jakarta.el.version>4.0.2</jakarta.el.version>
<antisamy.version>1.7.5</antisamy.version>
<snmp4j.version>3.8.0</snmp4j.version>
<langchain4j.version>1.1.0</langchain4j.version>
<langchain4j.version>1.8.0-TB</langchain4j.version>
<error_prone_annotations.version>2.38.0</error_prone_annotations.version>
<animal-sniffer-annotations.version>1.24</animal-sniffer-annotations.version>
<auto-value-annotations.version>1.11.0</auto-value-annotations.version>
@ -911,7 +911,7 @@
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
@ -1908,6 +1908,10 @@
<id>central</id>
<url>https://repo1.maven.org/maven2/</url>
</repository>
<repository>
<id>thingsboard-repo</id>
<url>https://repo.thingsboard.io/artifactory/libs-release-public</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>

2
rule-engine/rule-engine-api/pom.xml

@ -99,7 +99,7 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<dependency>

2
rule-engine/rule-engine-components/pom.xml

@ -154,7 +154,7 @@
<artifactId>json-path</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
</dependencies>

5
ui-ngx/package.json

@ -141,6 +141,9 @@
"rollup": "4.22.4",
"@babel/core": "7.25.2",
"esbuild": "0.23.0",
"jquery.terminal/coveralls-next/form-data": "4.0.4"
"jquery.terminal/coveralls-next/form-data": "4.0.4",
"node-forge": "1.3.3",
"glob": "10.5.0",
"path-to-regexp": "0.1.12"
}
}

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -192,6 +192,7 @@ export interface IStateController {
getStateIdAtIndex(index: number): string;
getEntityId(entityParamName: string): EntityId;
getCurrentStateName(): string;
reInit(): void;
}
export interface SubscriptionInfo {

4
ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts

@ -56,9 +56,7 @@ export class CheckConnectivityDialogComponent extends DialogComponent<CheckConne
},
chatModelConfig: {
modelType: ModelType.CHAT,
provider: this.data.AIModel.configuration.provider,
providerConfig: {...this.data.AIModel.configuration.providerConfig},
modelId: this.data.AIModel.configuration.modelId,
...this.data.AIModel.configuration,
maxRetries: 0,
timeoutSeconds: 20
}

16
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { Component, Inject, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -51,7 +51,7 @@ export interface AlarmDetailsDialogData {
templateUrl: './alarm-details-dialog.component.html',
styleUrls: ['./alarm-details-dialog.component.scss']
})
export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> implements OnInit {
export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> {
alarmId: string;
alarmFormGroup: UntypedFormGroup;
@ -128,13 +128,12 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
this.alarmFormGroup.get('startTime')
.patchValue(this.datePipe.transform(alarm.startTs, 'yyyy-MM-dd HH:mm:ss'));
}
if (alarm.startTs || alarm.endTs) {
if (alarm.startTs || alarm.clearTs) {
let duration = '';
if (alarm.startTs && (alarm.status === AlarmStatus.ACTIVE_ACK || alarm.status === AlarmStatus.ACTIVE_UNACK)) {
if (alarm.startTs && !alarm.cleared) {
duration = this.millisecondsToTimeStringPipe.transform(Date.now() - alarm.startTs);
}
if (alarm.endTs && (alarm.status === AlarmStatus.CLEARED_ACK || alarm.status === AlarmStatus.CLEARED_UNACK)) {
duration = this.millisecondsToTimeStringPipe.transform(alarm.endTs - alarm.startTs);
} else if (alarm.clearTs && alarm.cleared) {
duration = this.millisecondsToTimeStringPipe.transform(alarm.clearTs - alarm.startTs);
}
this.alarmFormGroup.get('duration').patchValue(duration);
}
@ -144,9 +143,6 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
this.alarmFormGroup.get('alarmDetails').patchValue(alarm.details);
}
ngOnInit(): void {
}
close(): void {
this.dialogRef.close(this.alarmUpdated);
}

10
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -198,6 +198,7 @@
<ng-container *ngIf="isEdit; else timewindowTemplate">
<tb-timewindow class="lt-xl:!hidden" asButton
flatButton
showSaveAsDefault
[displayTimewindowValue]="false"
[isEdit]="true"
tooltipPosition="below"
@ -207,6 +208,7 @@
</tb-timewindow>
<tb-timewindow class="gt-lg:!hidden"
hideLabel
showSaveAsDefault
[isEdit]="true"
tooltipPosition="below"
aggregation="true"
@ -222,14 +224,18 @@
tooltipPosition="below"
aggregation="true"
timezone="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow">
[showSaveAsDefault]="!readonly"
[(ngModel)]="dashboardCtx.dashboardTimewindow"
(saveAsDefault)="saveDashboard()">>
</tb-timewindow>
<tb-timewindow class="lt-lg:!hidden"
[isEdit]="false"
tooltipPosition="below"
aggregation="true"
timezone="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow">
[showSaveAsDefault]="!readonly"
[(ngModel)]="dashboardCtx.dashboardTimewindow"
(saveAsDefault)="saveDashboard()">>
</tb-timewindow>
</ng-container>
</ng-template>

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -1244,7 +1244,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
widgetEditMode: this.widgetEditMode,
singlePageMode: this.singlePageMode
};
const needReInitState = !this.isEdit;
this.init(dashboardPageInitData);
if (needReInitState) {
this.dashboardCtx.stateController.reInit();
}
} else {
this.dashboard.version = dashboard.version;
this.setEditMode(false, false);

2
ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts

@ -15,7 +15,6 @@
///
import { IStateController, StateObject } from '@core/api/widget-api.models';
import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models';
import { DashboardState } from '@shared/models/dashboard.models';
export declare type StateControllerState = StateObject[];
@ -29,6 +28,5 @@ export interface IStateControllerComponent extends IStateController {
states: {[id: string]: DashboardState };
dashboardId: string;
preservedState: any;
reInit(): void;
init(): void;
}

2
ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts

@ -176,7 +176,7 @@ export class MobileAppComponent extends EntityComponent<MobileApp> {
context: ctx,
showCloseButton: false,
popoverContentStyle: {padding: '16px 24px'},
isModal: false
isModal: true
});
releaseNotesPanelPopover.tbComponentRef.instance.popover = releaseNotesPanelPopover;
releaseNotesPanelPopover.tbComponentRef.instance.editorContentApplied.subscribe((releaseNotes) => {

30
ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts

@ -59,9 +59,33 @@ export class EditorPanelComponent implements OnInit {
resize: false,
setup: (editor) => {
editor.on('PostRender', function() {
const container = editor.getContainer().closest('.tb-popover-content');
const uiContainer = document.querySelector('.tox.tox-tinymce-aux');
container.parentNode.appendChild(uiContainer);
const container = document.querySelector('.tox.tox-tinymce-aux');
const styleSheet = document.createElement('style');
styleSheet.innerText = `
.tox-tiered-menu .tox-menu {
width: fit-content;
max-width: min(80%, 440px);
@media screen and (max-width: 510px) {
max-width: calc(100% - 64px);
}
media screen and (min-width: 511px) and (max-width: 548px) {
max-width: calc(100% - 84px);
}
media screen and (min-width: 549px) and (max-width: 599px) {
max-width: calc(100% - 104px);
}
}
.tox-tiered-menu .tox-menu .tox-collection__item-label {
word-break: normal;
}
@media screen and (max-width: 890px) {
.tox-tiered-menu > .tox-collection--list:not(:first-child) {
left: auto !important;
right: 0 !important;
}
}
`;
container.prepend(styleSheet);
});
},
relative_urls: false,

2
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html

@ -70,6 +70,7 @@
<ng-container *ngIf="targetNotificationForm.get('configuration.usersFilter.filterByTenants').value; else tenantProfiles">
<tb-entity-list
formControlName="tenantsIds"
syncIdsWithDB
subscriptSizing="dynamic"
labelText="{{ 'tenant.tenants' | translate }}"
placeholderText="{{ 'tenant.tenants' | translate }}"
@ -80,6 +81,7 @@
<ng-template #tenantProfiles>
<tb-entity-list
formControlName="tenantProfilesIds"
syncIdsWithDB
subscriptSizing="dynamic"
labelText="{{ 'tenant-profile.tenant-profiles' | translate }}"
placeholderText="{{ 'tenant-profile.tenant-profiles' | translate }}"

31
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts

@ -95,6 +95,37 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
autofocus: false,
branding: false,
promotion: false,
setup: (editor) => {
editor.on('PostRender', function() {
const container = document.querySelector('.tox.tox-tinymce-aux');
const styleSheet = document.createElement('style');
styleSheet.innerText = `
.tox-tiered-menu .tox-menu {
width: fit-content;
max-width: min(80%, 440px);
@media screen and (max-width: 510px) {
max-width: calc(100% - 64px);
}
media screen and (min-width: 511px) and (max-width: 548px) {
max-width: calc(100% - 84px);
}
media screen and (min-width: 549px) and (max-width: 599px) {
max-width: calc(100% - 104px);
}
}
.tox-tiered-menu .tox-menu .tox-collection__item-label {
word-break: normal;
}
@media screen and (max-width: 890px) {
.tox-tiered-menu > .tox-collection--list:not(:first-child) {
left: auto !important;
right: 0 !important;
}
}
`;
container.prepend(styleSheet);
});
},
relative_urls: false,
urlconverter_callback: (url) => url
};

10
ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html

@ -301,6 +301,16 @@
</tb-timezone-select>
</div>
</section>
@if (showSaveAsDefault) {
<section class="tb-form-panel">
<div class="tb-form-panel-title" translate>timewindow.save-current-settings-as-default</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="hideSaveAsDefault">
{{ 'timewindow.hide-option-from-end-users' | translate }}
</mat-slide-toggle>
</div>
</section>
}
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions class="tb-dialog-actions tb-flex flex-end no-gap">

13
ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts

@ -62,6 +62,7 @@ import {
export interface TimewindowConfigDialogData {
quickIntervalOnly: boolean;
aggregation: boolean;
showSaveAsDefault: boolean;
timewindow: Timewindow;
}
@ -76,6 +77,8 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
aggregation = false;
showSaveAsDefault = false;
timewindowForm: FormGroup;
historyTypes = HistoryWindowType;
@ -140,6 +143,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
super(store);
this.quickIntervalOnly = data.quickIntervalOnly;
this.aggregation = data.aggregation;
this.showSaveAsDefault = data.showSaveAsDefault;
this.timewindow = data.timewindow;
if (!this.quickIntervalOnly) {
@ -241,7 +245,9 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
hideAggInterval: [ isDefinedAndNotNull(this.timewindow.hideAggInterval)
? this.timewindow.hideAggInterval : false ],
hideTimezone: [ isDefinedAndNotNull(this.timewindow.hideTimezone)
? this.timewindow.hideTimezone : false ]
? this.timewindow.hideTimezone : false ],
hideSaveAsDefault: [ isDefinedAndNotNull(this.timewindow.hideSaveAsDefault)
? this.timewindow.hideSaveAsDefault : false ],
});
this.updateValidators(this.timewindowForm.get('aggregation.type').value);
@ -423,11 +429,6 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
realtimeDisableCustomInterval, historyDisableCustomInterval,
timewindowFormValue.realtime.advancedParams, timewindowFormValue.history.advancedParams,
this.realtimeTimewindowOptions, this.historyTimewindowOptions);
this.timewindowForm.patchValue({
hideAggregation: timewindowFormValue.hideAggregation,
hideAggInterval: timewindowFormValue.hideAggInterval,
hideTimezone: timewindowFormValue.hideTimezone
});
}
update() {

5
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -173,6 +173,11 @@
</ng-container>
</section>
</ng-container>
@if (saveAsDefaultAvailable) {
<mat-checkbox class="tb-timewindow-form-checkbox" [formControl]="saveTimewindowControl">
{{ 'timewindow.save-current-settings-as-default' | translate }}
</mat-checkbox>
}
</div>
<ng-template #timezoneSelectionPanel>

4
ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss

@ -39,5 +39,9 @@
&-settings-btn {
color: rgba(0, 0, 0, 0.54);
}
&-checkbox {
--mdc-checkbox-state-layer-size: 24px;
}
}
}

16
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -47,7 +47,7 @@ import {
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { FormControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { TimeService } from '@core/services/time.service';
import { deepClone, isDefined } from '@core/utils';
import { OverlayRef } from '@angular/cdk/overlay';
@ -70,6 +70,7 @@ export interface TimewindowPanelData {
timezone: boolean;
isEdit: boolean;
panelMode: boolean;
showSaveAsDefault?: boolean;
}
export const TIMEWINDOW_PANEL_DATA = new InjectionToken<any>('TimewindowPanelData');
@ -111,6 +112,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
aggregationTypes = AggregationType;
result: Timewindow;
saveTimewindow: boolean;
saveTimewindowControl: FormControl;
timewindowTypeOptions: ToggleHeaderOption[] = [{
name: this.translate.instant('timewindow.history'),
@ -126,6 +129,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
historyTypeSelectionAvailable: boolean;
historyIntervalSelectionAvailable: boolean;
aggregationOptionsAvailable: boolean;
saveAsDefaultAvailable: boolean;
realtimeDisableCustomInterval: boolean;
realtimeDisableCustomGroupInterval: boolean;
@ -220,6 +224,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
this.aggregationOptionsAvailable = this.aggregation && (this.isEdit ||
!(this.timewindow.hideAggregation && this.timewindow.hideAggInterval));
this.saveAsDefaultAvailable = this.data.showSaveAsDefault && (!this.timewindow.hideSaveAsDefault || this.isEdit);
}
ngOnInit(): void {
@ -262,6 +268,10 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
}
}
if (this.saveAsDefaultAvailable) {
this.saveTimewindowControl = this.fb.control({value: this.isEdit, disabled: this.isEdit});
}
this.timewindowForm = this.fb.group({
selectedTab: [isDefined(this.timewindow.selectedTab) ? this.timewindow.selectedTab : TimewindowType.REALTIME],
realtime: this.fb.group({
@ -409,6 +419,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
update() {
this.result = this.prepareTimewindowConfig();
this.saveTimewindow = this.saveAsDefaultAvailable && this.saveTimewindowControl.enabled && this.saveTimewindowControl.value;
this.overlayRef?.dispose();
}
@ -564,7 +575,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
data: {
quickIntervalOnly: this.quickIntervalOnly,
aggregation: this.aggregation,
timewindow: this.prepareTimewindowConfig(false)
timewindow: this.prepareTimewindowConfig(false),
showSaveAsDefault: this.data.showSaveAsDefault
}
}).afterClosed()
.subscribe((res) => {

20
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -19,12 +19,14 @@ import {
Component,
DestroyRef,
ElementRef,
EventEmitter,
forwardRef,
HostBinding,
Injector,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
StaticProvider,
ViewChild,
@ -189,6 +191,13 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
@coerceBoolean()
panelMode = true;
@Input()
@coerceBoolean()
showSaveAsDefault = false;
@Output()
saveAsDefault = new EventEmitter<Timewindow>();
innerValue: Timewindow;
timewindowDisabled: boolean;
@ -261,6 +270,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
timezone: this.timezone,
isEdit: this.isEdit,
panelMode: this.panelMode,
showSaveAsDefault: this.showSaveAsDefault,
} as TimewindowPanelData
},
{
@ -280,7 +290,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
this.innerValue = componentRef.instance.result;
this.timewindowDisabled = this.isTimewindowDisabled();
this.updateDisplayValue();
this.notifyChanged();
this.notifyChanged(this.showSaveAsDefault && componentRef.instance.saveTimewindow);
}
});
this.cd.detectChanges();
@ -334,8 +344,11 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
}
}
notifyChanged() {
notifyChanged(notifySaveAsDefault = false) {
this.propagateChange(cloneSelectedTimewindow(this.innerValue));
if (notifySaveAsDefault) {
this.saveAsDefault.emit(this.innerValue);
}
}
displayValue(): string {
@ -402,6 +415,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
timezone: this.timezone,
isEdit: this.isEdit,
panelMode: this.panelMode,
showSaveAsDefault: this.showSaveAsDefault,
}
const injector = Injector.create({
providers: [{ provide: TIMEWINDOW_PANEL_DATA, useValue: panelData }],
@ -413,7 +427,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
).subscribe(value => {
this.innerValue = value;
this.timewindowDisabled = this.isTimewindowDisabled();
this.notifyChanged();
this.notifyChanged(this.showSaveAsDefault && componentRef.instance.saveTimewindow);
})
}
}

9
ui-ngx/src/app/shared/models/ai-model.models.ts

@ -110,6 +110,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
'o3',
'o3-mini',
'o1',
'gpt-5.1',
'gpt-5',
'gpt-5-mini',
'gpt-5-nano',
@ -135,6 +136,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.GOOGLE_AI_GEMINI,
{
modelList: [
'gemini-3-pro-preview',
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-2.5-flash-lite',
@ -149,6 +151,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.GOOGLE_VERTEX_AI_GEMINI,
{
modelList: [
'gemini-3-pro-preview',
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-2.5-flash-lite',
@ -181,12 +184,10 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.ANTHROPIC,
{
modelList: [
'claude-opus-4-5',
'claude-opus-4-1',
'claude-opus-4-0',
'claude-sonnet-4-5',
'claude-sonnet-4-0',
'claude-3-7-sonnet-latest',
'claude-3-5-haiku-latest',
'claude-haiku-4-5'
],
providerFieldsList: ['apiKey'],
modelFieldsList: ['temperature', 'topP', 'topK', 'maxOutputTokens'],

7
ui-ngx/src/app/shared/models/time/time.models.ts

@ -169,6 +169,7 @@ export interface Timewindow {
history?: HistoryWindow;
aggregation?: Aggregation;
timezone?: string;
hideSaveAsDefault?: boolean;
}
export interface SubscriptionAggregation extends Aggregation {
@ -331,6 +332,9 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
if (value.hideTimezone) {
model.hideTimezone = value.hideTimezone;
}
if (value.hideSaveAsDefault) {
model.hideSaveAsDefault = value.hideSaveAsDefault;
}
model.selectedTab = getTimewindowType(value);
@ -1116,6 +1120,9 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => {
if (timewindow.hideTimezone) {
cloned.hideTimezone = timewindow.hideTimezone;
}
if (timewindow.hideSaveAsDefault) {
cloned.hideSaveAsDefault = timewindow.hideSaveAsDefault;
}
if (isDefined(timewindow.selectedTab)) {
cloned.selectedTab = timewindow.selectedTab;
}

12
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -6475,13 +6475,13 @@
"timewindow": {
"timewindow": "Time window",
"timewindow-settings": "Time window settings",
"years": "{ years, plural, =1 { year } other {# years } }",
"years": "{ years, plural, =1 {1 year } other {# years } }",
"years-short": "{{ years }}y",
"months": "{ months, plural, =1 { month } other {# months } }",
"months": "{ months, plural, =1 {1 month } other {# months } }",
"months-short": "{{ months }}M",
"weeks": "{ weeks, plural, =1 { week } other {# weeks } }",
"weeks": "{ weeks, plural, =1 {1 week } other {# weeks } }",
"weeks-short": "{{ weeks }}w",
"days": "{ days, plural, =1 { day } other {# days } }",
"days": "{ days, plural, =1 {1 day } other {# days } }",
"days-short": "{{ days }}d",
"hours": "{ hours, plural, =0 { hour } =1 {1 hour } other {# hours } }",
"hr": "{{ hr }} hr",
@ -6541,7 +6541,9 @@
"default-agg-interval": "Default grouping interval",
"edit-intervals-list-hint": "List of available interval options can be specified.",
"edit-grouping-intervals-list-hint": "It is possible to configure the grouping intervals list and default grouping interval.",
"all": "All"
"all": "All",
"save-current-settings-as-default": "Save current settings as default time window",
"hide-option-from-end-users": "Hide option from end-users"
},
"tooltip": {
"trigger": "Trigger",

78
ui-ngx/yarn.lock

@ -5604,11 +5604,6 @@ fs-minipass@^3.0.0:
dependencies:
minipass "^7.0.3"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@ -5738,10 +5733,10 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^10.2.2, glob@^10.3.10, glob@^10.3.3, glob@^10.3.7:
version "10.4.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
glob@10.5.0, glob@^10.2.2, glob@^10.3.10, glob@^10.3.3, glob@^10.3.7, glob@^7.1.3:
version "10.5.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
dependencies:
foreground-child "^3.1.0"
jackspeak "^3.1.2"
@ -5750,18 +5745,6 @@ glob@^10.2.2, glob@^10.3.10, glob@^10.3.3, glob@^10.3.7:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
glob@^7.1.3:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.1.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
global-prefix@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-4.0.0.tgz#e9cc79aab9be1d03287e156a3f912dd0895463ed"
@ -6104,24 +6087,16 @@ indent-string@^4.0.0:
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@4.1.3, ini@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795"
@ -7170,7 +7145,7 @@ minimatch@9.0.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^3.1.1, minimatch@^3.1.2:
minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@ -7493,10 +7468,10 @@ node-fetch@^3.3.2:
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-forge@^1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
node-forge@1.3.3, node-forge@^1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751"
integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==
node-gyp-build-optional-packages@5.2.2:
version "5.2.2"
@ -7711,13 +7686,6 @@ on-headers@~1.0.2:
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
onetime@^5.1.0, onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
@ -7982,11 +7950,6 @@ path-exists@^5.0.0:
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7"
integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@ -8005,10 +7968,10 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-to-regexp@0.1.10:
version "0.1.10"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
path-to-regexp@0.1.10, path-to-regexp@0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
path-type@^5.0.0:
version "5.0.0"
@ -10079,11 +10042,6 @@ wrap-ansi@^9.0.0:
string-width "^7.0.0"
strip-ansi "^7.1.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@^8.16.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"

Loading…
Cancel
Save