diff --git a/build.gradle b/build.gradle index e6dc290e..fd3c4809 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ plugins { id "com.webcohesion.enunciate" version "2.17.0" apply false id "io.freefair.lombok" version "8.6" apply false - id 'com.github.jk1.dependency-license-report' version '1.16' + id 'com.github.jk1.dependency-license-report' version '3.0.1' id "com.github.hierynomus.license-report" version "0.15.0" } @@ -54,7 +54,6 @@ licenseReport { // } // } - excludeGroups = ['javax.servlet.*'] excludeGroups = ['javax.servlet.*'] excludes = [ 'com.fasterxml.jackson:jackson-bom', @@ -63,7 +62,7 @@ licenseReport { ] configurations = ['runtimeClasspath'] - allowedLicensesFile = new File("$rootDir/allowed-lic.json") + allowedLicensesFile = new File("$rootDir/allowed-licenses.json") } checkLicense { @@ -167,7 +166,6 @@ configure(javaProjects) { dependency 'org.slf4j:slf4j-api:1.7.25' // resolving conflicts - dependency 'com.squareup.okhttp3:okhttp:3.3.1' dependency 'net.minidev:json-smart:2.5.2' dependency 'commons-logging:commons-logging:1.2' @@ -288,9 +286,17 @@ configure(javaProjects) { dependency 'org.apache.bcel:bcel:6.6.0' // AI integration - dependency 'dev.langchain4j:langchain4j:1.7.1' - dependency 'dev.langchain4j:langchain4j-azure-open-ai-spring-boot-starter:1.7.1-beta14' - dependency 'dev.langchain4j:langchain4j-spring-boot-starter:1.7.1-beta14' + dependency 'dev.langchain4j:langchain4j:1.8.0' + dependency 'dev.langchain4j:langchain4j-spring-boot-starter:1.8.0-beta15' + dependency 'dev.langchain4j:langchain4j-open-ai-official:1.8.0-beta15' + + // Conflict resolution + dependency 'com.google.errorprone:error_prone_annotations:2.33.0' + dependency 'org.jetbrains.kotlin:kotlin-stdlib:1.9.10' + dependency 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10' + dependency 'com.fasterxml:classmate:1.7.0' + dependency 'org.apache.httpcomponents.client5:httpclient5:5.5' + dependency 'org.apache.httpcomponents.core5:httpcore5:5.3.4' dependency 'org.reactivestreams:reactive-streams:1.0.4' diff --git a/guide/configuration.md b/guide/configuration.md index 4fccf60e..74a4b09a 100644 --- a/guide/configuration.md +++ b/guide/configuration.md @@ -163,4 +163,44 @@ services: - SECURITY_API-KEYS-PROVIDER_API-KEYS_0_KEY=TEST_API_SECRET - SECURITY_API-KEYS-PROVIDER_API-KEYS_0_USER=admin - SECURITY_API-KEYS-PROVIDER_API-KEYS_0_AUTHORITIES=TB_ALLOW_READ, TB_ALLOW_WRITE -``` \ No newline at end of file +``` + +## Gen AI Configuration + +TimeBase Web Administrator can generate QQL queries with the help of an external Gen AI provider. +The feature reads its settings from the `ai-api` section of `application.yaml`. + +Example config: + +```yaml +ai-api: + enabled: true + provider: AZURE_LEGACY # OPENAI | AZURE | AZURE_LEGACY | GITHUB + endpointUrl: "https://YOUR-RESOURCE-NAME.openai.azure.com" # base url of the ai api, may be ommitted for OPENAI and GITHUB providers + deploymentName: gpt-5-mini-2025-08-07 # deployment for QQL generation + embeddingDeploymentName: text-embedding-3-small-1 # deployment for embeddings + keys: # ai api keys per user + - username: admin + key: ${ADMIN_AI_KEY} # resolves from environment variable + - username: reader + key: READER_AI_KEY # takes priority over key from security.oauth2.users section + maxAttempts: 3 # max attempts allowed for an AI to produce a valid QQL query +``` + +If a user is not present in `ai-api.keys`, the system falls back to `security.oauth2.users[].aiApiKey` when it is defined. + +> [!IMPORTANT] +> The Gen AI endpoint must be compatible with the selected provider's API. + +Supported AI providers: + +1. AZURE_LEGACY includes the deployment name in the request URL. Example: + `https://YOUR-RESOURCE-NAME.openai.azure.com/openai/deployments/gpt-5-mini-2025-08-07/chat/completions` + where `https://YOUR-RESOURCE-NAME.openai.azure.com` is the `endpointUrl` and + `gpt-5-mini-2025-08-07` is the `deploymentName`. +2. AZURE uses the `endpointUrl` and provides the model name in the request body. +3. OPENAI is the official OpenAI API. The endpoint defaults to + `https://api.openai.com/v1`, but may be any compatible endpoint. +4. GITHUB uses the GitHub Models compatible API. The endpoint defaults to + `https://models.inference.ai.azure.com`. Use a GitHub personal access token + as the key — see GitHub docs [docs](https://docs.github.com/en/github-models/about-github-models) for details. diff --git a/java/ws-server/build.gradle b/java/ws-server/build.gradle index 114989b7..506cafc8 100644 --- a/java/ws-server/build.gradle +++ b/java/ws-server/build.gradle @@ -82,8 +82,8 @@ dependencies { // AI integration implementation 'dev.langchain4j:langchain4j' - implementation 'dev.langchain4j:langchain4j-azure-open-ai-spring-boot-starter' implementation 'dev.langchain4j:langchain4j-spring-boot-starter' + implementation 'dev.langchain4j:langchain4j-open-ai-official' compileOnly 'com.webcohesion.enunciate:enunciate-core-annotations' diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/GenAiConfig.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/GenAiConfig.java index 1c884a57..566ea617 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/GenAiConfig.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/GenAiConfig.java @@ -54,27 +54,21 @@ public class GenAiConfig { private static final String FALLBACK_EMB_FILE = "qql-docs-embeddings.json"; @Bean - public PerUserAzureChatModel perUserAzureChatModel(AiApiSettings settings, - UserAiApiKeyProvider keyProvider) { - return new PerUserAzureChatModel(settings.getEndpointUrl(), - settings.getDeploymentName(), - keyProvider); + public ChatModel perUserChatModel(AiApiSettings settings, + UserAiApiKeyProvider keyProvider) { + return new PerUserOpenAiOfficialChatModel(settings, keyProvider); } @Bean - public PerUserAzureStreamingChatModel perUserAzureStreamingChatModel(AiApiSettings settings, - UserAiApiKeyProvider keyProvider) { - return new PerUserAzureStreamingChatModel(settings.getEndpointUrl(), - settings.getDeploymentName(), - keyProvider); + public StreamingChatModel perUserStreamingChatModel(AiApiSettings settings, + UserAiApiKeyProvider keyProvider) { + return new PerUserOpenAiOfficialStreamingChatModel(settings, keyProvider); } @Bean - public PerUserAzureEmbeddingModel perUserAzureEmbeddingModel(AiApiSettings settings, - UserAiApiKeyProvider keyProvider) { - return new PerUserAzureEmbeddingModel(settings.getEndpointUrl(), - settings.getEmbeddingDeploymentName(), - keyProvider); + public EmbeddingModel perUserEmbeddingModel(AiApiSettings settings, + UserAiApiKeyProvider keyProvider) { + return new PerUserOpenAiOfficialEmbeddingModel(settings, keyProvider); } @Bean diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebMvcConfig.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebMvcConfig.java index 2642732e..18f1105e 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebMvcConfig.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebMvcConfig.java @@ -18,15 +18,19 @@ import com.epam.deltix.tbwg.webapp.interceptors.TimebaseLoginInterceptor; import com.epam.deltix.tbwg.webapp.interceptors.RestLogInterceptor; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncodingArgumentResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.util.UrlPathHelper; +import java.util.List; + @Configuration public class WebMvcConfig implements WebMvcConfigurer { @@ -37,6 +41,9 @@ public class WebMvcConfig implements WebMvcConfigurer { private final RestLogInterceptor logInterceptor; private final TimebaseLoginInterceptor timebaseLoginInterceptor; + @Autowired + private JsonBigIntEncodingArgumentResolver argumentResolver; + @Autowired public WebMvcConfig(AsyncTaskExecutor asyncTaskExecutor, RestLogInterceptor logInterceptor, @@ -65,4 +72,9 @@ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(logInterceptor); registry.addInterceptor(timebaseLoginInterceptor); } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(argumentResolver); + } } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorController.java index 159ba682..dfe4c7e1 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorController.java @@ -19,6 +19,7 @@ import com.epam.deltix.tbwg.webapp.config.WebSocketConfig; import com.epam.deltix.tbwg.webapp.services.timebase.MonitorService; import com.epam.deltix.tbwg.webapp.utils.HeaderAccessorHelper; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; import com.epam.deltix.tbwg.webapp.websockets.subscription.Subscription; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionController; @@ -53,8 +54,9 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor headerAccessor, Subscr long fromTimestamp = headerAccessorHelper.getTimestamp(headerAccessor); List symbols = headerAccessorHelper.getSymbols(headerAccessor); List types = headerAccessorHelper.getTypes(headerAccessor); + JsonBigIntEncoding bigIntEncoding = HeaderAccessorHelper.getJsonBigIntEncoding(headerAccessor); - monitorService.subscribe(sessionId, subscriptionId, stream, null, fromTimestamp, types, symbols, channel::sendMessage); + monitorService.subscribe(sessionId, subscriptionId, stream, null, fromTimestamp, types, symbols, channel::sendMessage, bigIntEncoding); return () -> monitorService.unsubscribe(sessionId, subscriptionId); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorQqlController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorQqlController.java index 471dee27..b60ef833 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorQqlController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/MonitorQqlController.java @@ -16,11 +16,10 @@ */ package com.epam.deltix.tbwg.webapp.controllers; -import com.epam.deltix.gflog.api.Log; -import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.tbwg.webapp.config.WebSocketConfig; import com.epam.deltix.tbwg.webapp.services.timebase.MonitorService; import com.epam.deltix.tbwg.webapp.utils.HeaderAccessorHelper; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; import com.epam.deltix.tbwg.webapp.websockets.subscription.Subscription; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionController; @@ -55,8 +54,9 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor headerAccessor, Subscr long fromTimestamp = headerAccessorHelper.getTimestamp(headerAccessor); List symbols = headerAccessorHelper.getSymbols(headerAccessor); List types = headerAccessorHelper.getTypes(headerAccessor); + JsonBigIntEncoding bigIntEncoding = HeaderAccessorHelper.getJsonBigIntEncoding(headerAccessor); - monitorService.subscribe(sessionId, subscriptionId, null, qql, fromTimestamp, types, symbols, channel::sendMessage); + monitorService.subscribe(sessionId, subscriptionId, null, qql, fromTimestamp, types, symbols, channel::sendMessage, bigIntEncoding); return () -> monitorService.unsubscribe(sessionId, subscriptionId); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimebaseController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimebaseController.java index 318390c9..21c6547e 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimebaseController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimebaseController.java @@ -25,6 +25,7 @@ import com.epam.deltix.qsrv.hf.tickdb.pub.lock.LockType; import com.epam.deltix.qsrv.hf.tickdb.ui.tbshell.TickDBShell; import com.epam.deltix.tbwg.webapp.model.smd.CurrencyDef; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; import com.epam.deltix.timebase.messages.IdentityKey; import com.epam.deltix.timebase.messages.InstrumentKey; import com.epam.deltix.timebase.messages.InstrumentMessage; @@ -175,14 +176,14 @@ public long correlationId() { */ @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/select", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity select(@Valid @RequestBody(required = false) SelectRequest select) - throws NoStreamsException { + public ResponseEntity select(@Valid @RequestBody(required = false) SelectRequest select, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { if (select == null) { select = new SelectRequest(); } return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) - .body(selectService.select(select, MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET)); + .body(selectService.select(select, MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET, bigIntEncoding)); } /** @@ -217,7 +218,8 @@ public ResponseEntity select( @RequestParam(required = false) Long offset, @RequestParam(required = false) Integer rows, @RequestParam(required = false) String space, - @RequestParam(required = false) boolean reverse) throws NoStreamsException { + @RequestParam(required = false) boolean reverse, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { SelectRequest request = new SelectRequest(); request.streams = streams; request.symbols = symbols; @@ -230,7 +232,7 @@ public ResponseEntity select( request.reverse = reverse; request.depth = depth; request.space = space; - return select(request); + return select(request, bigIntEncoding); } /** @@ -249,13 +251,14 @@ public ResponseEntity select( @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/select", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity select(@PathVariable String streamId, - @Valid @RequestBody(required = false) StreamRequest select) + @Valid @RequestBody(required = false) StreamRequest select, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { if (select == null) select = new StreamRequest(); return select(streamId, select.symbols, select.types, null, select.from, select.to, select.offset, - select.rows, select.space, select.reverse); + select.rows, select.space, select.reverse, bigIntEncoding); } /** @@ -297,11 +300,12 @@ public ResponseEntity select( @RequestParam(required = false) Long offset, @RequestParam(required = false) Integer rows, @RequestParam(required = false) String space, - @RequestParam(required = false) boolean reverse) throws NoStreamsException { + @RequestParam(required = false) boolean reverse, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { if (TextUtils.isEmpty(streamId)) throw new NoStreamsException(); - return select(new String[]{streamId}, symbols, types, depth, from, to, offset, rows, space, reverse); + return select(new String[]{streamId}, symbols, types, depth, from, to, offset, rows, space, reverse, bigIntEncoding); } // download operation is permitted for any user @@ -1025,7 +1029,7 @@ ResponseEntity checkWritable(String error) { @RequestMapping(value = "/{streamId}/{symbolId}/select", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity select(@PathVariable String streamId, @PathVariable String symbolId, @Valid @RequestBody(required = false) InstrumentRequest select, - OutputStream outputStream) { + OutputStream outputStream, JsonBigIntEncoding bigIntEncoding) { DXTickStream stream = service.getStream(streamId); if (stream == null) @@ -1054,7 +1058,7 @@ public ResponseEntity select(@PathVariable String streamI .contentType(MediaType.APPLICATION_JSON) .body(new MessageSource2ResponseStream( stream.select(startTime, options, select.types, ids), select.getEndTime(), startIndex, endIndex, - MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET) + MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET, bigIntEncoding) ); } @@ -1090,14 +1094,15 @@ public ResponseEntity select( @RequestParam(required = false) Long offset, @RequestParam(required = false) Integer rows, @RequestParam(required = false) String space, - @RequestParam(required = false) boolean reverse) throws NoStreamsException { + @RequestParam(required = false) boolean reverse, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { if (TextUtils.isEmpty(streamId)) throw new NoStreamsException(); if (TextUtils.isEmpty(symbolId)) return ResponseEntity.notFound().build(); - return select(new String[]{streamId}, new String[]{symbolId}, types, depth, from, to, offset, rows, space, reverse); + return select(new String[]{streamId}, new String[]{symbolId}, types, depth, from, to, offset, rows, space, reverse, bigIntEncoding); } private SelectionOptions getSelectionOption(BaseRequest r) { @@ -1535,9 +1540,8 @@ public ResponseEntity streams(@RequestParam(required = false, defau */ @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/query", method = {RequestMethod.POST}) - public ResponseEntity query(Principal principal, @Valid @RequestBody(required = false) QueryRequest select) - throws InvalidQueryException, WriteOperationsException { - + public ResponseEntity query(Principal principal, @Valid @RequestBody(required = false) QueryRequest select, + JsonBigIntEncoding bigIntEncoding) throws InvalidQueryException, WriteOperationsException { if (select == null || StringUtils.isEmpty(select.query)) throw new InvalidQueryException(select == null ? "" : select.query); @@ -1559,7 +1563,34 @@ public ResponseEntity query(Principal principal, @Valid @ .body(new MessageSource2ResponseStream( service.getConnection().executeQuery( select.query, options, null, null, select.getStartTime(Long.MIN_VALUE), select.getEndTime(Long.MIN_VALUE)), - select.getEndTime(), startIndex, endIndex, MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET)); + select.getEndTime(), startIndex, endIndex, MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET, bigIntEncoding)); + } + + /** + * Executes an QQL query and returns the maximum possible number of records + */ + @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") + @RequestMapping(value = "/unlimitedQuery", method = {RequestMethod.POST}) + public ResponseEntity unlimitedQuery(Principal principal, @Valid @RequestBody(required = false) QueryRequest select, + JsonBigIntEncoding bigIntEncoding) + throws InvalidQueryException, WriteOperationsException { + + if (select == null || StringUtils.isEmpty(select.query)) + throw new InvalidQueryException(select == null ? "" : select.query); + if (service.isReadonly() && (select.query.toLowerCase().contains("drop") || select.query.toLowerCase().contains("create"))) + throw new WriteOperationsException("CREATE or DROP"); + if (isDdlQuery(select.query) && !hasAuthority(principal, "TB_ALLOW_WRITE")) { + throw new AccessDeniedException("TB_ALLOW_WRITE permission required."); + } + + SelectionOptions options = getSelectionOption(select); + LOGGER.info().append("UNLIMITED QUERY: (").append(select.query).append(")").commit(); + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(new MessageSource2ResponseStream( + service.getConnection().executeQuery( + select.query, options, null, null, select.getStartTime(Long.MIN_VALUE), select.getEndTime(Long.MIN_VALUE)), + select.getEndTime(), 0, Integer.MAX_VALUE, Integer.MAX_VALUE, bigIntEncoding)); } private boolean isDdlQuery(String query) { @@ -1663,8 +1694,8 @@ public Set queryFunctionsShort() { @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/filter", method = {RequestMethod.POST}, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity filter(@PathVariable String streamId, @Valid @RequestBody FilterRequest filter) - throws UnknownStreamException { + public ResponseEntity filter(@PathVariable String streamId, @Valid @RequestBody FilterRequest filter, + JsonBigIntEncoding bigIntEncoding) throws UnknownStreamException { DXTickStream stream = service.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -1697,7 +1728,7 @@ public ResponseEntity filter(@PathVariable String streamI .contentType(MediaType.APPLICATION_JSON) .body(new MessageSource2ResponseStream(service.getConnection() .executeQuery(query, options, null, null, startTime), endTime, startIndex, endIndex, - MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET)); + MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET, bigIntEncoding)); } /** diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TopicController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TopicController.java index 59658e5a..240c89da 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TopicController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TopicController.java @@ -28,6 +28,8 @@ import com.epam.deltix.tbwg.webapp.model.tree.TreeNodeDef; import com.epam.deltix.tbwg.webapp.services.timebase.MonitorService; import com.epam.deltix.tbwg.webapp.services.topic.TopicService; +import com.epam.deltix.tbwg.webapp.utils.HeaderAccessorHelper; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; import com.epam.deltix.tbwg.webapp.websockets.subscription.Subscription; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionController; @@ -126,8 +128,9 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor header, SubscriptionCh String topicKey = URLDecoder.decode(extractId(destination), StandardCharsets.UTF_8); String sessionId = header.getSessionId(); String subscriptionId = header.getSubscriptionId(); + JsonBigIntEncoding bigIntEncoding = HeaderAccessorHelper.getJsonBigIntEncoding(header); - monitorService.subscribeTopic(sessionId, subscriptionId, topicKey, channel::sendMessage); + monitorService.subscribeTopic(sessionId, subscriptionId, topicKey, channel::sendMessage, bigIntEncoding); return () -> monitorService.unsubscribe(sessionId, subscriptionId); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/transformations/QqlConversionTransformation.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/transformations/QqlConversionTransformation.java index e7106c4c..568dab29 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/transformations/QqlConversionTransformation.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/transformations/QqlConversionTransformation.java @@ -20,13 +20,15 @@ import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; import com.epam.deltix.tbwg.messages.Message; import com.epam.deltix.tbwg.webapp.model.charting.line.RawElementDef; +import com.epam.deltix.tbwg.webapp.utils.json.WebGatewayJsonRawMessagePrinterFactory; import java.util.Collections; +@Deprecated // not implemented on the front-end side public class QqlConversionTransformation extends AbstractChartTransformation { private final StringBuilder sb = new StringBuilder(); - private final JSONRawMessagePrinter rawMessagePrinter = new JSONRawMessagePrinter(); + private final JSONRawMessagePrinter rawMessagePrinter = WebGatewayJsonRawMessagePrinterFactory.create(); public QqlConversionTransformation() { super(Collections.singletonList(RawMessage.class), Collections.singletonList(RawElementDef.class)); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/GenAiService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/GenAiService.java index f5ec9232..f45a0ecd 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/GenAiService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/GenAiService.java @@ -30,6 +30,8 @@ import com.epam.deltix.tbwg.webapp.settings.AiApiSettings; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel; import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.chat.response.PartialResponse; +import dev.langchain4j.model.chat.response.PartialResponseContext; import dev.langchain4j.service.Result; import dev.langchain4j.service.TokenStream; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -158,8 +160,16 @@ private void iterativeGenerate(String username, String schema, String pinned, String wrappedPrompt = PerUserChatMaker.wrapUserInput(username, prompt); TokenStream ts = genAiHelperService.genQql(schema, pinned, wrappedPrompt) - .onPartialResponse((String part) -> { - if (!ctx.stopped().get() && part != null && !part.isEmpty()) { + .onPartialResponseWithContext((PartialResponse partResp, PartialResponseContext partCtx) -> { + if (ctx.stopped().get()) { + partCtx.streamingHandle().cancel(); + latch.countDown(); + LOG.info("Generation cancelled by user."); + return; + } + + String part = partResp.text(); + if (part != null && !part.isEmpty()) { streamed.append(part); send(ch, QqlGenMessage.builder("PART") .data(part)); @@ -175,7 +185,6 @@ private void iterativeGenerate(String username, String schema, String pinned, .error(e.getMessage()).finalEvent(true)); latch.countDown(); }); - ts.start(); try { if (!latch.await(STREAM_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)) { @@ -223,7 +232,7 @@ private void send(SubscriptionChannel ch, QqlGenMessage.Builder b) { ch.sendMessage(b.build()); } - private String loadOverview() { + private static String loadOverview() { try { return Resources.toString( Resources.getResource("qql_gen/overview.md"), @@ -273,19 +282,19 @@ private static void describeBasic(DXTickStream stream, StringBuilder b) { b.append("\n\n"); } - private String repairPrompt(String intent, String prev, String err) { + private static String repairPrompt(String intent, String prev, String err) { return "User intent:\n" + intent + "\n\nPrevious QQL (fix minimally):\n" + prev + "\n\nCompiler error:\n" + err + "\n\nOutput ONLY corrected QQL."; } - private String summaryPinned(String pinned) { + private static String summaryPinned(String pinned) { if (pinned == null) return "No pinned docs."; return "Pinned size=" + pinned.length(); } - private Set parseStreamKeys(String raw) { + private static Set parseStreamKeys(String raw) { if (raw == null || raw.isBlank()) return Collections.emptySet(); return Stream.of(raw.split(",")) .map(String::trim) diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureStreamingChatModel.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureStreamingChatModel.java deleted file mode 100644 index 68ff2812..00000000 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureStreamingChatModel.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2025 EPAM Systems, Inc - * - * See the NOTICE file distributed with this work for additional information - * regarding copyright ownership. 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 com.epam.deltix.tbwg.webapp.services.genai.models; - -import dev.langchain4j.model.azure.AzureOpenAiStreamingChatModel; -import dev.langchain4j.model.chat.StreamingChatModel; -import dev.langchain4j.model.chat.request.ChatRequest; -import dev.langchain4j.model.chat.request.ChatRequestParameters; -import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; - -public class PerUserAzureStreamingChatModel implements StreamingChatModel { - - private final String endpoint; - private final String deploymentName; - private final UserAiApiKeyProvider keyProvider; - private final ChatRequestParameters defaultParams; - - public PerUserAzureStreamingChatModel(String endpoint, - String deploymentName, - UserAiApiKeyProvider keyProvider) { - this.endpoint = endpoint; - this.deploymentName = deploymentName; - this.keyProvider = keyProvider; - this.defaultParams = PerUserChatMaker.defaultParams(deploymentName); - } - - @Override - public ChatRequestParameters defaultRequestParameters() { - return defaultParams; - } - - @Override - public void doChat(ChatRequest chatRequest, StreamingChatResponseHandler handler) { - var pr = PerUserChatMaker.prepare(chatRequest); - String username = pr.username(); - ChatRequest effectiveRequest = pr.request(); - - StreamingChatModel model = AzureOpenAiStreamingChatModel.builder() - .endpoint(endpoint) - .deploymentName(deploymentName) - .apiKey(keyProvider.resolve(username)) - .build(); - - model.doChat(effectiveRequest, handler); - } -} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserChatMaker.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserChatMaker.java index 1980184b..b7c11a4f 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserChatMaker.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserChatMaker.java @@ -20,6 +20,7 @@ import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ChatRequestParameters; +import dev.langchain4j.model.openaiofficial.OpenAiOfficialChatRequestParameters; import java.util.ArrayList; import java.util.List; @@ -105,4 +106,8 @@ public static ProcessResult prepare(ChatRequest chatRequest) { public static ChatRequestParameters defaultParams(String modelName) { return ChatRequestParameters.builder().modelName(modelName).build(); } + + public static OpenAiOfficialChatRequestParameters defaultOpenAiOfficialParams(String modelName) { + return OpenAiOfficialChatRequestParameters.builder().modelName(modelName).build(); + } } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureChatModel.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialChatModel.java similarity index 50% rename from java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureChatModel.java rename to java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialChatModel.java index 829be143..a0c38589 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureChatModel.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialChatModel.java @@ -16,26 +16,24 @@ */ package com.epam.deltix.tbwg.webapp.services.genai.models; -import dev.langchain4j.model.azure.AzureOpenAiChatModel; +import com.epam.deltix.tbwg.webapp.settings.AiApiSettings; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ChatRequestParameters; import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.openaiofficial.OpenAiOfficialChatModel; -public class PerUserAzureChatModel implements ChatModel { +public class PerUserOpenAiOfficialChatModel implements ChatModel { - private final String endpoint; - private final String deploymentName; + private final AiApiSettings settings; private final UserAiApiKeyProvider keyProvider; private final ChatRequestParameters defaultParams; - public PerUserAzureChatModel(String endpoint, - String deploymentName, - UserAiApiKeyProvider keyProvider) { - this.endpoint = endpoint; - this.deploymentName = deploymentName; + public PerUserOpenAiOfficialChatModel(AiApiSettings settings, + UserAiApiKeyProvider keyProvider) { + this.settings = settings; this.keyProvider = keyProvider; - this.defaultParams = PerUserChatMaker.defaultParams(deploymentName); + this.defaultParams = PerUserChatMaker.defaultOpenAiOfficialParams(settings.chatModelNameForParams()); } @Override @@ -49,13 +47,33 @@ public ChatResponse doChat(ChatRequest chatRequest) { String username = pr.username(); ChatRequest effectiveRequest = pr.request(); - ChatModel model = AzureOpenAiChatModel.builder() - .endpoint(endpoint) - .apiKey(keyProvider.resolve(username)) - .deploymentName(deploymentName) - .build(); + ChatModel model = createDelegate(username); return model.doChat(effectiveRequest); } + private ChatModel createDelegate(String username) { + OpenAiOfficialChatModel.Builder builder = OpenAiOfficialChatModel.builder() + .apiKey(keyProvider.resolve(username)); + + String endpoint = settings.getEndpointUrl(); + if (endpoint != null && !endpoint.isBlank()) { + builder.baseUrl(endpoint); + } + + String deployment = settings.requireDeploymentName(); + switch (settings.providerOrDefault()) { + case OPENAI -> builder.modelName(deployment); + case AZURE -> builder.isAzure(true) + .modelName(deployment) + .azureDeploymentName(deployment); + case AZURE_LEGACY -> builder.isAzure(true) + .azureDeploymentName(deployment) + .modelName(""); + case GITHUB -> builder.isGitHubModels(true) + .modelName(deployment); + } + + return builder.build(); + } } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureEmbeddingModel.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialEmbeddingModel.java similarity index 57% rename from java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureEmbeddingModel.java rename to java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialEmbeddingModel.java index c1bfdf9f..b0dccd19 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserAzureEmbeddingModel.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialEmbeddingModel.java @@ -16,26 +16,24 @@ */ package com.epam.deltix.tbwg.webapp.services.genai.models; +import com.epam.deltix.tbwg.webapp.settings.AiApiSettings; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; -import dev.langchain4j.model.azure.AzureOpenAiEmbeddingModel; import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.openaiofficial.OpenAiOfficialEmbeddingModel; import dev.langchain4j.model.output.Response; import java.util.ArrayList; import java.util.List; -public class PerUserAzureEmbeddingModel implements EmbeddingModel { +public class PerUserOpenAiOfficialEmbeddingModel implements EmbeddingModel { - private final String endpoint; - private final String deploymentName; + private final AiApiSettings settings; private final UserAiApiKeyProvider keyProvider; - public PerUserAzureEmbeddingModel(String endpoint, - String deploymentName, - UserAiApiKeyProvider keyProvider) { - this.endpoint = endpoint; - this.deploymentName = deploymentName; + public PerUserOpenAiOfficialEmbeddingModel(AiApiSettings settings, + UserAiApiKeyProvider keyProvider) { + this.settings = settings; this.keyProvider = keyProvider; } @@ -60,11 +58,32 @@ public Response> embedAll(List textSegments) { effectiveSegments.add(TextSegment.from(unwrappedText, segment.metadata())); } - EmbeddingModel model = AzureOpenAiEmbeddingModel.builder() - .endpoint(endpoint) - .deploymentName(deploymentName) - .apiKey(keyProvider.resolve(username)) - .build(); + EmbeddingModel model = createDelegate(username); return model.embedAll(effectiveSegments); } + + private EmbeddingModel createDelegate(String username) { + OpenAiOfficialEmbeddingModel.Builder builder = OpenAiOfficialEmbeddingModel.builder() + .apiKey(keyProvider.resolve(username)); + + String endpoint = settings.getEndpointUrl(); + if (endpoint != null && !endpoint.isBlank()) { + builder.baseUrl(endpoint); + } + + String deployment = settings.requireEmbeddingDeploymentName(); + switch (settings.providerOrDefault()) { + case OPENAI -> builder.modelName(deployment); + case AZURE -> builder.isAzure(true) + .modelName(deployment) + .azureDeploymentName(deployment); + case AZURE_LEGACY -> builder.isAzure(true) + .azureDeploymentName(deployment) + .modelName(""); + case GITHUB -> builder.isGitHubModels(true) + .modelName(deployment); + } + + return builder.build(); + } } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialStreamingChatModel.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialStreamingChatModel.java new file mode 100644 index 00000000..cec18a20 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/models/PerUserOpenAiOfficialStreamingChatModel.java @@ -0,0 +1,80 @@ +/* + * Copyright 2025 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. 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 com.epam.deltix.tbwg.webapp.services.genai.models; + +import com.epam.deltix.tbwg.webapp.settings.AiApiSettings; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.request.ChatRequestParameters; +import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; +import dev.langchain4j.model.openaiofficial.OpenAiOfficialStreamingChatModel; + +public class PerUserOpenAiOfficialStreamingChatModel implements StreamingChatModel { + + private final AiApiSettings settings; + private final UserAiApiKeyProvider keyProvider; + private final ChatRequestParameters defaultParams; + + public PerUserOpenAiOfficialStreamingChatModel(AiApiSettings settings, + UserAiApiKeyProvider keyProvider) { + this.settings = settings; + this.keyProvider = keyProvider; + this.defaultParams = PerUserChatMaker + .defaultOpenAiOfficialParams(settings.chatModelNameForParams()); + } + + @Override + public ChatRequestParameters defaultRequestParameters() { + return defaultParams; + } + + @Override + public void doChat(ChatRequest chatRequest, StreamingChatResponseHandler handler) { + var pr = PerUserChatMaker.prepare(chatRequest); + String username = pr.username(); + ChatRequest effectiveRequest = pr.request(); + + StreamingChatModel model = createDelegate(username); + + model.doChat(effectiveRequest, handler); + } + + private StreamingChatModel createDelegate(String username) { + OpenAiOfficialStreamingChatModel.Builder builder = OpenAiOfficialStreamingChatModel.builder() + .apiKey(keyProvider.resolve(username)); + + String endpoint = settings.getEndpointUrl(); + if (endpoint != null && !endpoint.isBlank()) { + builder.baseUrl(endpoint); + } + + String deployment = settings.requireDeploymentName(); + switch (settings.providerOrDefault()) { + case OPENAI -> builder.modelName(deployment); + case AZURE -> builder.isAzure(true) + .modelName(deployment) + .azureDeploymentName(deployment); + case AZURE_LEGACY -> builder.isAzure(true) + .azureDeploymentName(deployment) + .modelName(""); + case GITHUB -> builder.isGitHubModels(true) + .modelName(deployment); + } + + return builder.build(); + } +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorService.java index 7c4188b6..e0a0d225 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorService.java @@ -16,16 +16,18 @@ */ package com.epam.deltix.tbwg.webapp.services.timebase; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; + import java.util.List; import java.util.function.Consumer; public interface MonitorService { void subscribe(String sessionId, String subscriptionId, String key, String qql, long fromTimestamp, List types, - List symbols, Consumer consumer); + List symbols, Consumer consumer, JsonBigIntEncoding bigIntEncoding); void subscribeTopic(String sessionId, String subscriptionId, String key, - Consumer consumer); + Consumer consumer, JsonBigIntEncoding bigIntEncoding); void unsubscribe(String sessionId, String subscriptionId); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorServiceImpl.java index ede2acc5..1f32feb4 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/MonitorServiceImpl.java @@ -16,13 +16,12 @@ */ package com.epam.deltix.tbwg.webapp.services.timebase; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; +import com.epam.deltix.tbwg.webapp.utils.json.WebGatewayJsonRawMessagePrinterFactory; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import com.epam.deltix.qsrv.hf.pub.RawMessage; -import com.epam.deltix.qsrv.util.json.DataEncoding; import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; -import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinterFactory; -import com.epam.deltix.qsrv.util.json.PrintType; import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; import com.epam.deltix.tbwg.webapp.utils.cache.CachedMessageBufferImpl; import org.springframework.stereotype.Service; @@ -53,9 +52,9 @@ public MonitorServiceImpl(TimebaseService timebase) { @Override public synchronized void subscribe(String sessionId, String subscriptionId, String key, String qql, long fromTimestamp, List types, - List symbols, Consumer consumer) + List symbols, Consumer consumer, JsonBigIntEncoding bigIntEncoding) { - BufferedConsumer bufferedConsumer = new BufferedConsumer(); + BufferedConsumer bufferedConsumer = new BufferedConsumer(WebGatewayJsonRawMessagePrinterFactory.create(bigIntEncoding)); StreamConsumer streamConsumer = new StreamConsumer(timebase, fromTimestamp, key, qql, symbols, types, bufferedConsumer); ScheduledFuture scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> { String messages = bufferedConsumer.messageBuffer.flush(); @@ -70,9 +69,9 @@ public synchronized void subscribe(String sessionId, String subscriptionId, Stri @Override public synchronized void subscribeTopic(String sessionId, String subscriptionId, String key, - Consumer consumer) + Consumer consumer, JsonBigIntEncoding bigIntEncoding) { - BufferedConsumer bufferedConsumer = new BufferedConsumer(); + BufferedConsumer bufferedConsumer = new BufferedConsumer(WebGatewayJsonRawMessagePrinterFactory.create(bigIntEncoding)); TopicConsumer topicConsumer = new TopicConsumer(timebase, key, bufferedConsumer); ScheduledFuture scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> { String messages = bufferedConsumer.messageBuffer.flush(); @@ -102,10 +101,11 @@ public synchronized void preDestroy() { private static final class BufferedConsumer implements Consumer { - private final JSONRawMessagePrinter printer = - new JSONRawMessagePrinter(false, true,DataEncoding.STANDARD, true, true,PrintType.FULL, "$type"); + private final CachedMessageBufferImpl messageBuffer; - private final CachedMessageBufferImpl messageBuffer = new CachedMessageBufferImpl(printer); + public BufferedConsumer(JSONRawMessagePrinter printer) { + messageBuffer = new CachedMessageBufferImpl(printer); + } @Override public void accept(RawMessage rawMessage) { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SelectServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SelectServiceImpl.java index a2a4a447..67f7070c 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SelectServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SelectServiceImpl.java @@ -20,6 +20,7 @@ import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.qsrv.hf.pub.ChannelQualityOfService; import com.epam.deltix.tbwg.webapp.services.timebase.base.SelectService; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; import com.epam.deltix.timebase.messages.IdentityKey; import com.epam.deltix.qsrv.hf.tickdb.pub.DXTickStream; import com.epam.deltix.qsrv.hf.tickdb.pub.SelectionOptions; @@ -52,7 +53,8 @@ public SelectServiceImpl(TimebaseService timebase) { @Override public MessageSource2ResponseStream select(long startTime, long endTime, long offset, int rows, boolean reverse, - String[] types, String[] symbols, String[] keys, String space, int maxRecords) + String[] types, String[] symbols, String[] keys, String space, int maxRecords, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { List streams = getStreams(keys); @@ -81,15 +83,15 @@ public MessageSource2ResponseStream select(long startTime, long endTime, long of .append("AND timestamp [").append(GMT.formatDateTimeMillis(startTime)).append(":") .append(GMT.formatDateTimeMillis(endTime)).append("]") .commit(); - return new MessageSource2ResponseStream(source, endTime, startIndex, endIndex, maxRecords); + return new MessageSource2ResponseStream(source, endTime, startIndex, endIndex, maxRecords, bigIntEncoding); } @Override - public MessageSource2ResponseStream select(SelectRequest selectRequest, int maxRecords) throws NoStreamsException { + public MessageSource2ResponseStream select(SelectRequest selectRequest, int maxRecords, JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { List streams = getStreams(selectRequest.streams); long startTime = selectRequest.getStartTime(getEndTime(streams)); return select(startTime, selectRequest.getEndTime(), selectRequest.offset, selectRequest.rows, selectRequest.reverse, - selectRequest.types, selectRequest.symbols, selectRequest.streams, selectRequest.space, maxRecords); + selectRequest.types, selectRequest.symbols, selectRequest.streams, selectRequest.space, maxRecords, bigIntEncoding); } private List getStreams(String ... streamKeys) throws NoStreamsException { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SelectService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SelectService.java index 2d25a54c..1970cab7 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SelectService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SelectService.java @@ -19,13 +19,15 @@ import com.epam.deltix.tbwg.webapp.model.input.SelectRequest; import com.epam.deltix.tbwg.webapp.services.timebase.exc.NoStreamsException; import com.epam.deltix.tbwg.webapp.utils.MessageSource2ResponseStream; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; public interface SelectService { MessageSource2ResponseStream select(long startTime, long endTime, long offset, int rows, boolean reverse, - String[] types, String[] symbols, String[] keys, String space, int maxRecords) + String[] types, String[] symbols, String[] keys, String space, int maxRecords, + JsonBigIntEncoding bigIntEncoding) throws NoStreamsException; - MessageSource2ResponseStream select(SelectRequest selectRequest, int maxRecords) throws NoStreamsException; + MessageSource2ResponseStream select(SelectRequest selectRequest, int maxRecords, JsonBigIntEncoding bigIntEncoding) throws NoStreamsException; } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/AiApiSettings.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/AiApiSettings.java index 26f35bf9..7265cf9f 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/AiApiSettings.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/AiApiSettings.java @@ -35,10 +35,18 @@ public class AiApiSettings { private boolean enabled; private String endpointUrl; private List keys; + private Provider provider = Provider.AZURE_LEGACY; private String deploymentName; private String embeddingDeploymentName; private int maxAttempts; + public enum Provider { + OPENAI, + AZURE, + AZURE_LEGACY, + GITHUB + } + @Getter @Setter public static class UserKey { @@ -46,6 +54,33 @@ public static class UserKey { private String key; } + public Provider providerOrDefault() { + return provider == null ? Provider.AZURE_LEGACY : provider; + } + + public String chatModelNameForParams() { + return providerOrDefault() == Provider.AZURE_LEGACY ? "" : deploymentName; + } + + public String embeddingModelNameForParams() { + return providerOrDefault() == Provider.AZURE_LEGACY ? "" : embeddingDeploymentName; + } + + public String requireDeploymentName() { + return requireNonBlank(deploymentName, "ai-api.deploymentName"); + } + + public String requireEmbeddingDeploymentName() { + return requireNonBlank(embeddingDeploymentName, "ai-api.embeddingDeploymentName"); + } + + private static String requireNonBlank(String value, String property) { + if (value == null || value.isBlank()) { + throw new IllegalStateException(property + " must be provided for the selected ai-api.provider"); + } + return value; + } + public String findUserKey(String username) { if (username == null || keys == null) return null; return keys.stream() diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/LocaleSettings.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/LocaleSettings.java new file mode 100644 index 00000000..c2a118c4 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/LocaleSettings.java @@ -0,0 +1,35 @@ +package com.epam.deltix.tbwg.webapp.settings; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class LocaleSettings { + + @Value("${spring.web.locale:}") + private String locale; + + private static Locale applicationLocale; + + public static Locale getApplicationLocale() { + return applicationLocale; + } + + @PostConstruct + private void setup() { + Set available = Arrays.stream(Locale.getAvailableLocales()).map(Locale::toLanguageTag).collect(Collectors.toSet()); + if (available.contains(locale)) { + applicationLocale = Locale.forLanguageTag(locale); + } else { + applicationLocale = Locale.getDefault(); + } + } + + +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/HeaderAccessorHelper.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/HeaderAccessorHelper.java index cf48bf0d..cb47624e 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/HeaderAccessorHelper.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/HeaderAccessorHelper.java @@ -16,6 +16,8 @@ */ package com.epam.deltix.tbwg.webapp.utils; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncodingArgumentResolver; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.epam.deltix.gflog.api.Log; @@ -47,6 +49,19 @@ public long getTimestamp(SimpMessageHeaderAccessor headerAccessor) { return Long.MIN_VALUE; } + public static JsonBigIntEncoding getJsonBigIntEncoding(SimpMessageHeaderAccessor accessor) { + String headerValue = accessor.getFirstNativeHeader(JsonBigIntEncodingArgumentResolver.BIG_INT_ENCODING_HEADER); + if (headerValue != null) { + try { + return JsonBigIntEncoding.valueOf(headerValue.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + LOG.warn("Unknown value for %s: '%s', using default: %s") + .with(JsonBigIntEncodingArgumentResolver.BIG_INT_ENCODING_HEADER).with(headerValue).with(JsonBigIntEncodingArgumentResolver.DEFAULT_ENCODING); + } + } + return JsonBigIntEncodingArgumentResolver.DEFAULT_ENCODING; + } + public List getSymbols(SimpMessageHeaderAccessor headerAccessor) { List headers = headerAccessor.getNativeHeader(SYMBOLS_HEADER); if (headers == null || headers.isEmpty()) { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/MessageSource2ResponseStream.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/MessageSource2ResponseStream.java index 7f8f2725..34fdb760 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/MessageSource2ResponseStream.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/MessageSource2ResponseStream.java @@ -20,9 +20,9 @@ import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.qsrv.hf.pub.RawMessage; import com.epam.deltix.qsrv.hf.tickdb.pub.query.InstrumentMessageSource; -import com.epam.deltix.qsrv.util.json.DataEncoding; import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; -import com.epam.deltix.qsrv.util.json.PrintType; +import com.epam.deltix.tbwg.webapp.utils.json.JsonBigIntEncoding; +import com.epam.deltix.tbwg.webapp.utils.json.WebGatewayJsonRawMessagePrinterFactory; import com.epam.deltix.util.lang.Util; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; @@ -41,28 +41,28 @@ public class MessageSource2ResponseStream implements StreamingResponseBody { private final long endIndex; // inclusive private final int maxRecords; - private final JSONRawMessagePrinter printer = - new JSONRawMessagePrinter(false, true, DataEncoding.STANDARD, true, - false, PrintType.FULL, true, "$type"); + private final JSONRawMessagePrinter printer; private final StringBuilder sb = new StringBuilder(); @SuppressWarnings({"unused"}) - public MessageSource2ResponseStream(InstrumentMessageSource source, int maxRecords) { + public MessageSource2ResponseStream(InstrumentMessageSource source, int maxRecords, JsonBigIntEncoding bigIntEncoding) { this.source = source; this.toTimestamp = Long.MAX_VALUE; this.startIndex = 0; this.endIndex = Integer.MAX_VALUE; this.maxRecords = maxRecords; + this.printer = WebGatewayJsonRawMessagePrinterFactory.create(bigIntEncoding); } public MessageSource2ResponseStream(InstrumentMessageSource messageSource, long toTimestamp, long startIndex, - long endIndex, int maxRecords) { + long endIndex, int maxRecords, JsonBigIntEncoding bigIntEncoding) { this.source = messageSource; this.toTimestamp = toTimestamp; this.startIndex = startIndex; this.endIndex = endIndex; this.maxRecords = maxRecords; + this.printer = WebGatewayJsonRawMessagePrinterFactory.create(bigIntEncoding); } @Override diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/CustomEncodingJsonRawMessagePrinter.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/CustomEncodingJsonRawMessagePrinter.java new file mode 100644 index 00000000..c3f04fd7 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/CustomEncodingJsonRawMessagePrinter.java @@ -0,0 +1,76 @@ +package com.epam.deltix.tbwg.webapp.utils.json; + +import com.epam.deltix.qsrv.hf.pub.NullValueException; +import com.epam.deltix.qsrv.hf.pub.ReadableValue; +import com.epam.deltix.qsrv.hf.pub.codec.NonStaticFieldInfo; +import com.epam.deltix.qsrv.hf.pub.md.ArrayDataType; +import com.epam.deltix.qsrv.hf.pub.md.DataType; +import com.epam.deltix.qsrv.hf.pub.md.IntegerDataType; +import com.epam.deltix.qsrv.util.json.DataEncoding; +import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; +import com.epam.deltix.qsrv.util.json.PrintType; + + +import java.util.Locale; + +public class CustomEncodingJsonRawMessagePrinter extends JSONRawMessagePrinter { + + + /** + * This printer only supports STANDARD DataEncoding. + * There are optimizations present in this implementation that may produce incorrect results + * if used with other DataEncoding values. + */ + public CustomEncodingJsonRawMessagePrinter(Locale locale) { + super(false, true, DataEncoding.STANDARD, true, true, PrintType.FULL, false, "$type", locale); + } + + protected boolean appendFieldValue(ReadableValue decoder, NonStaticFieldInfo field, StringBuilder sb) { + DataType dataType = field.getType(); + if (dataType instanceof IntegerDataType && ((IntegerDataType) dataType).getSize() == 8) { + try { + long v = decoder.getLong(); + appendString(String.valueOf(v), sb); + return ((IntegerDataType) dataType).getNullValue() != v; + } catch (NullValueException e) { + return false; + } + } else { + return super.appendFieldValue(decoder, field, sb); + } + } + + protected boolean appendArrayField(ArrayDataType type, ReadableValue udec, StringBuilder sb) throws NullValueException { + final DataType underlineType = type.getElementDataType(); + if (underlineType instanceof IntegerDataType && ((IntegerDataType) underlineType).getSize() == 8) { + final int len = udec.getArrayLength(); + appendBlock('[', sb); + boolean needSepa = false; + for (int i = 0; i < len; i++) { + try { + final ReadableValue rv = udec.nextReadableElement(); + if (needSepa) + appendSeparator(sb); + else + needSepa = true; + sb.append(rv.getString()); + } catch (NullValueException e) { + sb.append("null"); + } + } + appendBlock(']', sb); + return true; + } else { + return super.appendArrayField(type, udec, sb); + } + } + + protected void appendStaticValue(DataType dataType, String value, StringBuilder sb) { + if (dataType instanceof IntegerDataType && ((IntegerDataType) dataType).getSize() == 8) { + sb.append('"').append(value).append('"'); + } else { + super.appendStaticValue(dataType, value, sb); + } + } + +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncoding.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncoding.java new file mode 100644 index 00000000..850aafaa --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncoding.java @@ -0,0 +1,6 @@ +package com.epam.deltix.tbwg.webapp.utils.json; + +public enum JsonBigIntEncoding { + STRING, + NUMBER +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncodingArgumentResolver.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncodingArgumentResolver.java new file mode 100644 index 00000000..def88f57 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/JsonBigIntEncodingArgumentResolver.java @@ -0,0 +1,38 @@ +package com.epam.deltix.tbwg.webapp.utils.json; + +import com.epam.deltix.gflog.api.Log; +import com.epam.deltix.gflog.api.LogFactory; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class JsonBigIntEncodingArgumentResolver implements HandlerMethodArgumentResolver { + + private static final Log LOG = LogFactory.getLog(JsonBigIntEncodingArgumentResolver.class); + public static final String BIG_INT_ENCODING_HEADER = "X-JSON-BigInt-Encoding"; + public static final JsonBigIntEncoding DEFAULT_ENCODING = JsonBigIntEncoding.NUMBER; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType() == JsonBigIntEncoding.class; + } + + @Override + public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, org.springframework.web.bind.support.WebDataBinderFactory binderFactory) { + String headerValue = webRequest.getHeader(BIG_INT_ENCODING_HEADER); + if (headerValue != null) { + try { + return JsonBigIntEncoding.valueOf(headerValue.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + LOG.warn("Unknown value for %s: '%s', using default: %s") + .with(BIG_INT_ENCODING_HEADER).with(headerValue).with(DEFAULT_ENCODING); + } + } + return DEFAULT_ENCODING; + } +} \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/WebGatewayJsonRawMessagePrinterFactory.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/WebGatewayJsonRawMessagePrinterFactory.java new file mode 100644 index 00000000..d5c245e2 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/utils/json/WebGatewayJsonRawMessagePrinterFactory.java @@ -0,0 +1,22 @@ +package com.epam.deltix.tbwg.webapp.utils.json; + +import com.epam.deltix.qsrv.util.json.DataEncoding; +import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; +import com.epam.deltix.qsrv.util.json.PrintType; +import com.epam.deltix.tbwg.webapp.settings.LocaleSettings; + +import java.util.Locale; + +public class WebGatewayJsonRawMessagePrinterFactory { + + public static JSONRawMessagePrinter create() { + return create(JsonBigIntEncoding.NUMBER); + } + public static JSONRawMessagePrinter create(JsonBigIntEncoding bigIntEncoding) { + Locale locale = LocaleSettings.getApplicationLocale(); + if (bigIntEncoding == JsonBigIntEncoding.STRING) { + return new CustomEncodingJsonRawMessagePrinter(locale); + } + return new JSONRawMessagePrinter(false, true, DataEncoding.STANDARD, true, true, PrintType.FULL, false, "$type", locale); + } +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSHandler.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSHandler.java index 560e45ac..4ab31983 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSHandler.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSHandler.java @@ -24,6 +24,7 @@ import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.timebase.connections.TbUserDetails; import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; +import com.epam.deltix.tbwg.webapp.utils.json.WebGatewayJsonRawMessagePrinterFactory; import com.epam.deltix.timebase.messages.IdentityKey; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -32,9 +33,7 @@ import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.qsrv.hf.pub.RawMessage; import com.epam.deltix.qsrv.hf.tickdb.pub.topic.DirectChannel; -import com.epam.deltix.qsrv.util.json.DataEncoding; import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; -import com.epam.deltix.qsrv.util.json.PrintType; import com.epam.deltix.tbwg.webapp.utils.cache.CachedMessageBufferImpl; import com.epam.deltix.tbwg.webapp.utils.cache.MessageBuffer; import com.epam.deltix.tbwg.webapp.utils.cache.MessageBufferImpl; @@ -79,11 +78,6 @@ public class WSHandler extends TextWebSocketHandler { protected final class PumpTask extends QuickExecutor.QuickTask { final Runnable avlnr = PumpTask.this::submit; - private final JSONRawMessagePrinter printer - = new JSONRawMessagePrinter(false, true, DataEncoding.STANDARD, true, true, PrintType.FULL, "$type"); - - //final JSONRawMessagePrinter printer = new JSONRawMessagePrinter(false, true); - private final TickCursor cursor; private final DXTickStream[] selection; private final IntermittentlyAvailableCursor c; @@ -104,6 +98,7 @@ public PumpTask (DXTickStream[] selection, TickCursor cursor, long toTimestamp, this.c = (IntermittentlyAvailableCursor)cursor; this.toTimestamp = toTimestamp; this.session = session; + JSONRawMessagePrinter printer = WebGatewayJsonRawMessagePrinterFactory.create(); this.buffer = useCache() ? new CachedMessageBufferImpl(printer) : new MessageBufferImpl(printer, live); sendCounter = metrics.endpointCounter(WebSocketConfig.SEND_MESSAGES_METRIC, endpoint()); diff --git a/java/ws-server/src/main/resources/application.yaml b/java/ws-server/src/main/resources/application.yaml index 075a36ca..8a6edebf 100644 --- a/java/ws-server/src/main/resources/application.yaml +++ b/java/ws-server/src/main/resources/application.yaml @@ -78,12 +78,13 @@ security: ai-api: enabled: true - endpointUrl: "" # endpoint must support Azure OpenAI API + provider: AZURE_LEGACY # OPENAI | AZURE | AZURE_LEGACY | GITHUB + endpointUrl: ${DIAL_ENDPOINT} # endpoint must support selected provider's API deploymentName: gpt-5-mini-2025-08-07 # deployment for QQL generation embeddingDeploymentName: text-embedding-3-small-1 # deployment for embeddings keys: # ai api keys per user - username: admin - key: ${ADMIN_AI_KEY} # resolves from environment variable + key: ${DIAL_API_KEY} # resolves from environment variable - username: reader key: READER_AI_KEY # takes priority over key from security.oauth2.users section maxAttempts: 3 # max attempts allowed for an AI to produce a valid QQL query diff --git a/web/frontend/package.json b/web/frontend/package.json index ec9130fd..800d69d9 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -52,6 +52,7 @@ "fast-deep-equal": "^3.1.3", "global": "^4.4.0", "gzip-js": "^0.3.2", + "json-bigint": "^1.0.0", "jszip": "^3.10.1", "luxon": "^1.25.0", "lz-string": "^1.4.4", @@ -81,10 +82,10 @@ "@deltix/hd.components-worker": "file:./libs/@deltix/hd.components-worker", "@deltix/hd.components-di": "file:./libs/@deltix/hd.components-di", "@deltix/ng-autocomplete": "file:./libs/@deltix/ng-autocomplete", - "@deltix/hd-date": "file:./libs/@deltix/hd-date", + "@deltix/hd-date": "file:./libs/@deltix/hd-date", "@deltix/ngx-vizceral": "file:./libs/@deltix/ngx-vizceral", "@deltix/vizceral": "file:./libs/@deltix/vizceral", - "@deltix/sso-auth": "file:./libs/@deltix/sso-auth" + "@deltix/sso-auth": "file:./libs/@deltix/sso-auth" }, "peerDependencies": { "postcss": "^8.0.0" diff --git a/web/frontend/src/app/core/services/interceptors/interceptors.module.ts b/web/frontend/src/app/core/services/interceptors/interceptors.module.ts index a91ee77c..ab8c5f1d 100644 --- a/web/frontend/src/app/core/services/interceptors/interceptors.module.ts +++ b/web/frontend/src/app/core/services/interceptors/interceptors.module.ts @@ -4,6 +4,7 @@ import {ApiPrefixesInterceptor} from './api-prefixes.interceptor'; import {AttachTokenInterceptor} from './attach-token.interceptor'; import {CatchConnectionErrorInterceptor} from './catch-connection-error.interceptor'; import {RequestDefaultErrorInterceptor} from './request-default-error.interceptor'; +import { AcceptBigIntFormatInterceptor } from './js-header.interceptor'; @NgModule({ providers: [ @@ -11,6 +12,7 @@ import {RequestDefaultErrorInterceptor} from './request-default-error.intercepto {provide: HTTP_INTERCEPTORS, useClass: AttachTokenInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: ApiPrefixesInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: CatchConnectionErrorInterceptor, multi: true}, + {provide: HTTP_INTERCEPTORS, useClass: AcceptBigIntFormatInterceptor, multi: true}, ], }) export class InterceptorsModule {} diff --git a/web/frontend/src/app/core/services/interceptors/js-header.interceptor.ts b/web/frontend/src/app/core/services/interceptors/js-header.interceptor.ts new file mode 100644 index 00000000..d325f8ae --- /dev/null +++ b/web/frontend/src/app/core/services/interceptors/js-header.interceptor.ts @@ -0,0 +1,40 @@ +import { + HttpEvent, + HttpHandler, + HttpHeaders, + HttpInterceptor, + HttpRequest, +} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; + +@Injectable() +export class AcceptBigIntFormatInterceptor implements HttpInterceptor { + constructor() {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + if (req.url.includes('select') || req.url.includes('query') || req.url.includes('filter')) { + const reqUrl = ((req.url[0] !== '/' && req.url[0] !== '.') || + (req.url[0] === '.' && req.url[1] !== '/')) && + req.url.search('http') < 0 + ? '/' + req.url + : req.url; + + const headers = {}; + + req.headers.keys().forEach((key) => { + headers[key] = req.headers.get(key); + }); + + headers['X-JSON-BigInt-Encoding'] = 'string'; + + return next.handle( + req.clone({ + url: reqUrl, + headers: new HttpHeaders(headers), + }), + ); + } + return next.handle(req.clone()); + } +} diff --git a/web/frontend/src/app/core/services/ws.service.ts b/web/frontend/src/app/core/services/ws.service.ts index 82c5c6a0..b4744589 100644 --- a/web/frontend/src/app/core/services/ws.service.ts +++ b/web/frontend/src/app/core/services/ws.service.ts @@ -42,6 +42,10 @@ export class WSService extends RxStomp implements OnDestroy { headers.ack = 'auto'; } + if (!headers['X-JSON-BigInt-Encoding'] && destination.includes('monitor')) { + headers['X-JSON-BigInt-Encoding'] = 'string'; + } + if (!unsubscribeHeaders) { unsubscribeHeaders = { destination: destination, diff --git a/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.html b/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.html index 034ca40a..47b0b187 100644 --- a/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.html +++ b/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.html @@ -12,7 +12,7 @@ - + diff --git a/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.scss b/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.scss index c3316049..a3aac80a 100644 --- a/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.scss +++ b/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.scss @@ -120,10 +120,11 @@ form { } .error-or-warning { - height: 2.5rem; + height: 3.2rem; overflow: hidden; visibility: hidden; margin-bottom: 0.3rem; + overflow-y: auto; } .error-message { @@ -254,3 +255,15 @@ ng-multiselect-dropdown { content: ' . . .'; } } + +.history-status { + margin-left: 1.9rem; + color: rgba(245, 245, 245, 0.4); +} + +pre { + font-family: inherit; + font-size: inherit; + font-weight: inherit; + color: #fff; +} diff --git a/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.ts b/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.ts index b8cdf40f..58c1914c 100644 --- a/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.ts +++ b/web/frontend/src/app/pages/generate-ddl/generate-ddl.component.ts @@ -1,212 +1,225 @@ -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { AbstractControl, FormBuilder, FormControl, FormGroup } from '@angular/forms'; -import { Subject, Subscription } from 'rxjs'; -import { distinctUntilChanged, map, takeUntil, tap } from 'rxjs/operators'; -import { TimebaseService } from './generate-ddl.service'; -import { AppState } from 'src/app/core/store'; -import { Store, select } from '@ngrx/store'; -import { getActiveTab } from '../streams/store/streams-tabs/streams-tabs.selectors'; -import { StreamsService } from 'src/app/shared/services/streams.service'; -import { StreamModel } from '../streams/models/stream.model'; -import { Router } from '@angular/router'; -import { appRoute } from 'src/app/shared/utils/routes.names'; - -const generationStatuses = { - PLANNING_START: "Starting to plan the response", - PLANning_DONE: "Planning complete", - TAGS: "System processing/Configuration check", - PLANNING_FAILED: "Planning failed", - DOCS_RETRIEVED: "Information found", - ERROR: "An error occurred", - ATTEMPT_START: "Starting response generation", - PART: "Generating response", - ATTEMPT_COMPILE_OK: "Response successfully compiled", - FINAL_SUCCESS: "Response ready", - ATTEMPT_COMPILE_ERROR: "Error during compilation", - FINAL_FAILURE: "Generation failed", - CANCELLED: "Canceled" -} as const; - -@Component({ - selector: 'app-generate-ddl', - templateUrl: './generate-ddl.component.html', - styleUrls: ['./generate-ddl.component.scss'], - standalone: false -}) -export class GenerateDDLComponent implements OnInit, OnDestroy { - inputDDL: FormControl; - resultStatus: typeof generationStatuses[keyof typeof generationStatuses]; - resultQuery = ''; - ddlErrorMessage: string | null; - qqlErrorMessage: string | null; - isLoading: boolean; - responceCame: boolean; - warningMessage: string; - form: FormGroup; - streamList: StreamModel[]; - streamNameList: string[]; - selectedStreamList: AbstractControl; - inputQuery: AbstractControl; - private destroy$ = new Subject(); - private tabId: string; - private querySubscription: Subscription; - - constructor( - private timebaseService: TimebaseService, - private cdRef: ChangeDetectorRef, - private appStore: Store, - private fb: FormBuilder, - private streamsService: StreamsService, - private router: Router - ) {} - - ngOnInit() { - this.inputDDL = new FormControl(null); - - this.form = this.fb.group({ - selectedStreams: [[]], - inputQuery: '' - }); - - this.selectedStreamList = this.form.get('selectedStreams'); - this.inputQuery = this.form.get('inputQuery'); - - this.streamsService.getList(false) - .pipe( - tap(streams => this.streamList = streams ?? []), - map(streams => streams.map(stream => stream.name) ?? []), - takeUntil(this.destroy$) - ) - .subscribe(streamNameList => this.streamNameList = streamNameList.filter(name => !!name.trim())); - - this.applySavedResult(); - - this.inputDDL.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe(value => { - const [, output, error, warning] = this.timebaseService.getSavedResult(`ddl-${this.tabId}`) ?? [null, null]; - this.timebaseService.saveResult(`ddl-${this.tabId}`, JSON.stringify([value, output, error, warning])); - }); - - this.inputQuery.valueChanges - .pipe(distinctUntilChanged(), takeUntil(this.destroy$)) - .subscribe(value => { - const [, output, error, warning, streams] = this.timebaseService.getSavedResult(`qql-${this.tabId}`) ?? [null, null]; - this.timebaseService.saveResult(`qql-${this.tabId}`, JSON.stringify([value, output, error, warning, streams])); - }); - - this.selectedStreamList.valueChanges - .pipe(takeUntil(this.destroy$)) - .subscribe(streams => { - const [input, output, error, warning] = this.timebaseService.getSavedResult(`qql-${this.tabId}`) ?? [null, null]; - this.timebaseService.saveResult( - `qql-${this.tabId}`, JSON.stringify([input, output, error, warning, streams])); - }); - } - - generate() { - this.responceCame = false; - this.resultQuery = ''; - - this.qqlErrorMessage = null; - this.isLoading = true; - this.cdRef.detectChanges(); - this.inputQuery.disable(); - this.resultStatus = null; - const streams = (this.streamList ?? []) - .filter(stream => this.selectedStreamList.value.includes(stream.name)) - .map(stream => stream.key); - - this.querySubscription = this.timebaseService.generateQQL(this.inputQuery.value, streams) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: result => { - this.responceCame = !!result.finalEvent; - this.qqlErrorMessage = result.error; - if (!this.responceCame) { - this.resultStatus = generationStatuses[result.stage]; - } - - if (result.stage === 'PART') { - this.resultQuery += result.data; - } else if (result.stage === 'FINAL_SUCCESS') { - this.resultQuery = result.data; - } else if (result.stage === 'ATTEMPT_START') { - this.resultQuery = ''; - } - - this.timebaseService.saveResult( - `qql-${this.tabId}`, - JSON.stringify([ - this.inputQuery.value, this.resultQuery, - this.qqlErrorMessage, this.selectedStreamList.value])); - - if (this.responceCame) { - this.responseCompleted(); - } - - this.cdRef.detectChanges(); - }, - error: () => { - this.inputQuery.enable(); - this.isLoading = false; - this.cdRef.detectChanges(); - this.resultQuery = ''; - this.timebaseService.saveResult( - `qql-${this.tabId}`, - JSON.stringify([this.inputQuery.value, this.resultQuery, null, null, this.selectedStreamList.value])); - } - }); - } - - private responseCompleted() { - this.resultStatus = null; - this.inputQuery.enable(); - this.isLoading = false; - this.querySubscription.unsubscribe(); - - this.timebaseService.saveResult( - `qql-${this.tabId}`, - JSON.stringify([ - this.inputQuery.value, this.resultQuery, - this.qqlErrorMessage, this.selectedStreamList.value])); - } - - stopGeneration() { - this.resultQuery = ''; - this.qqlErrorMessage = ''; - this.responseCompleted(); - } - - openQueryEditor() { - this.timebaseService.currentQuery = this.resultQuery; - this.router.navigate([appRoute, 'query']); - } - - setWarningMessage() { - this.warningMessage = `Failed to produce valid query. - The query below is provided just for reference and can't be used right away`; - } - - private applySavedResult() { - this.appStore.pipe(select(getActiveTab)) - .pipe( - distinctUntilChanged((tab1, tab2) => tab1?.id === tab2?.id), - takeUntil(this.destroy$) - ).subscribe(({ id }) => { - this.tabId = id; - const [inputQuery, output, error, streams] = this.timebaseService.getSavedResult(`qql-${id}`) ?? [null, null]; - this.inputQuery.patchValue(inputQuery ?? '', { emitEvent: false }); - this.resultQuery = output; - this.selectedStreamList.patchValue(streams ?? [], { emitEvent: false }); - this.qqlErrorMessage = error; - - this.setWarningMessage(); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } -} +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { AbstractControl, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { Subject, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter, map, take, takeUntil, tap } from 'rxjs/operators'; +import { GenerateQueryService } from './generate-ddl.service'; +import { AppState } from 'src/app/core/store'; +import { Store, select } from '@ngrx/store'; +import { getActiveTab, getActiveTabSettings } from '../streams/store/streams-tabs/streams-tabs.selectors'; +import { StreamsService } from 'src/app/shared/services/streams.service'; +import { StreamModel } from '../streams/models/stream.model'; +import { Router } from '@angular/router'; +import { appRoute } from 'src/app/shared/utils/routes.names'; +import { generationStatuses } from './types'; +import { SetTabSettings } from '../streams/store/streams-tabs/streams-tabs.actions'; +import { TabModel } from '../streams/models/tab.model'; + +@Component({ + selector: 'app-generate-ddl', + templateUrl: './generate-ddl.component.html', + styleUrls: ['./generate-ddl.component.scss'], + standalone: false +}) +export class GenerateDDLComponent implements OnInit, OnDestroy { + inputDDL: FormControl; + resultStatus: typeof generationStatuses[keyof typeof generationStatuses]; + generationStatuses: typeof generationStatuses[keyof typeof generationStatuses][] = []; + resultQuery = ''; + ddlErrorMessage: string | null; + qqlErrorMessage: string | null; + isLoading: boolean; + responceCame: boolean; + warningMessage: string; + form: FormGroup; + streamList: StreamModel[]; + streamNameList: string[]; + selectedStreamList: AbstractControl; + inputQuery: AbstractControl; + private destroy$ = new Subject(); + private tabId: string; + private querySubscription: Subscription; + + constructor( + private generateQueryService: GenerateQueryService, + private cdRef: ChangeDetectorRef, + private appStore: Store, + private fb: FormBuilder, + private streamsService: StreamsService, + private router: Router + ) { } + + ngOnInit() { + this.inputDDL = new FormControl(null); + + this.form = this.fb.group({ + selectedStreams: [[]], + inputQuery: '' + }); + + this.selectedStreamList = this.form.get('selectedStreams'); + this.inputQuery = this.form.get('inputQuery'); + + this.streamsService.getList(false) + .pipe( + tap(streams => this.streamList = streams ?? []), + map(streams => streams.map(stream => stream.name) ?? []), + takeUntil(this.destroy$) + ) + .subscribe(streamNameList => this.streamNameList = streamNameList.filter(name => !!name.trim())); + + this.applySavedResult(); + + this.inputDDL.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => { + const [, output, error, warning] = this.generateQueryService.getSavedResult(`ddl-${this.tabId}`) ?? [null, null]; + this.generateQueryService.saveResult(`ddl-${this.tabId}`, JSON.stringify([value, output, error, warning])); + }); + + this.inputQuery.valueChanges + .pipe(distinctUntilChanged(), takeUntil(this.destroy$)) + .subscribe(value => { + const [, output, error, warning, streams] = this.generateQueryService.getSavedResult(`qql-${this.tabId}`) ?? [null, null]; + this.generateQueryService.saveResult(`qql-${this.tabId}`, JSON.stringify([value, output, error, warning, streams])); + }); + + this.selectedStreamList.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(streams => { + const [input, output, error, warning] = this.generateQueryService.getSavedResult(`qql-${this.tabId}`) ?? [null, null]; + this.generateQueryService.saveResult( + `qql-${this.tabId}`, JSON.stringify([input, output, error, warning, streams])); + }); + } + + generate() { + this.responceCame = false; + this.resultQuery = ''; + + this.qqlErrorMessage = null; + this.isLoading = true; + this.cdRef.detectChanges(); + this.inputQuery.disable(); + this.resultStatus = null; + this.updateTabSettings(true); + this.generationStatuses = []; + const streams = (this.streamList ?? []) + .filter(stream => this.selectedStreamList.value.includes(stream.name)) + .map(stream => stream.key); + + this.querySubscription = this.generateQueryService.generateQQL(this.inputQuery.value, streams) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: result => { + this.responceCame = !!result.finalEvent; + this.qqlErrorMessage = result.error; + if (!this.responceCame) { + const newStatus = !this.generationStatuses.includes(this.resultStatus) && generationStatuses[result.stage] !== this.resultStatus; + if (this.resultStatus && newStatus) { + this.generationStatuses.unshift(this.resultStatus); + } + this.resultStatus = generationStatuses[result.stage]; + } + + if (result.stage === 'PART') { + this.resultQuery += result.data; + } else if (result.stage === 'FINAL_SUCCESS') { + this.resultQuery = result.data; + } else if (result.stage === 'ATTEMPT_START') { + this.resultQuery = ''; + } + + this.generateQueryService.saveResult( + `qql-${this.tabId}`, + JSON.stringify([ + this.inputQuery.value, this.resultQuery, + this.qqlErrorMessage, this.selectedStreamList.value])); + + if (this.responceCame) { + this.updateTabSettings(false); + this.responseCompleted(); + } + + this.cdRef.detectChanges(); + }, + error: () => { + this.inputQuery.enable(); + this.isLoading = false; + this.cdRef.detectChanges(); + this.resultQuery = ''; + this.generateQueryService.saveResult( + `qql-${this.tabId}`, + JSON.stringify([this.inputQuery.value, this.resultQuery, null, null, this.selectedStreamList.value])); + this.updateTabSettings(false); + } + }); + } + + private responseCompleted() { + this.resultStatus = null; + this.generationStatuses = []; + this.inputQuery.enable(); + this.isLoading = false; + this.querySubscription.unsubscribe(); + + this.generateQueryService.saveResult( + `qql-${this.tabId}`, + JSON.stringify([ + this.inputQuery.value, this.resultQuery, + this.qqlErrorMessage, this.selectedStreamList.value])); + } + + stopGeneration() { + this.resultQuery = ''; + this.qqlErrorMessage = ''; + this.responseCompleted(); + } + + openQueryEditor() { + this.generateQueryService.currentQuery = this.resultQuery; + this.router.navigate([appRoute, 'query']); + } + + setWarningMessage() { + this.warningMessage = `Failed to produce valid query. + The query below is provided just for reference and can't be used right away`; + } + + private applySavedResult() { + this.appStore.pipe(select(getActiveTab)) + .pipe( + filter(Boolean), + distinctUntilChanged((tab1: TabModel, tab2: TabModel) => tab1?.id === tab2?.id), + takeUntil(this.destroy$) + ).subscribe(({ id }) => { + this.tabId = id; + const [inputQuery, output, error, streams] = this.generateQueryService.getSavedResult(`qql-${id}`) ?? [null, null]; + this.inputQuery.patchValue(inputQuery ?? '', { emitEvent: false }); + this.resultQuery = output; + this.selectedStreamList.patchValue(streams ?? [], { emitEvent: false }); + this.qqlErrorMessage = error; + + this.setWarningMessage(); + }); + } + + private updateTabSettings(needAlert: boolean) { + this.appStore.pipe(select(getActiveTabSettings)) + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe(activeTabSettings => { + const tabSettings = { ...activeTabSettings }; + if (needAlert) { + tabSettings._showOnCloseAlerts = needAlert; + } else { + delete tabSettings._showOnCloseAlerts; + } + this.appStore.dispatch(new SetTabSettings({ tabSettings })); + } + ); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/web/frontend/src/app/pages/generate-ddl/generate-ddl.service.ts b/web/frontend/src/app/pages/generate-ddl/generate-ddl.service.ts index 748d253b..1e3fc39c 100644 --- a/web/frontend/src/app/pages/generate-ddl/generate-ddl.service.ts +++ b/web/frontend/src/app/pages/generate-ddl/generate-ddl.service.ts @@ -1,38 +1,33 @@ -import { Injectable } from '@angular/core'; -import { StompHeaders } from '@stomp/stompjs'; -import { map } from 'rxjs/operators'; -import { WSService } from 'src/app/core/services/ws.service'; - -@Injectable({ - providedIn: 'root', -}) -export class TimebaseService { - - currentQuery: string; - ddlIsUsing = false; - - constructor(private wsService: WSService) { } - - generateQQL(inputDDL: string, streamKeys: string[]) { - const stompHeaders: StompHeaders = { - userInput: inputDDL.replace(/\n/g, ' '), - streamKeys: streamKeys.join(',') - }; - - return this.wsService.watch('/user/topic/genai-qql', stompHeaders).pipe(map((ws_message) => JSON.parse(ws_message.body))); - } - - saveResult(key: string, value: string) { - sessionStorage.setItem(key, value); - } - - getSavedResult(key: string) { - return JSON.parse(sessionStorage.getItem(key)); - } -} - -export interface GenerateDDLResponce { - errorMessage: string | null, - resultDDL: string, - resultIsNotValid: boolean -} +import { Injectable } from '@angular/core'; +import { StompHeaders } from '@stomp/stompjs'; +import { map } from 'rxjs/operators'; +import { WSService } from 'src/app/core/services/ws.service'; + +@Injectable({ + providedIn: 'root', +}) +export class GenerateQueryService { + + currentQuery: string; + ddlIsUsing = false; + + constructor(private wsService: WSService) { } + + generateQQL(inputDDL: string, streamKeys: string[]) { + const stompHeaders: StompHeaders = { + userInput: inputDDL.replace(/\n/g, ' '), + streamKeys: streamKeys.join(',') + }; + + return this.wsService.watch('/user/topic/genai-qql', stompHeaders).pipe(map((ws_message) => JSON.parse(ws_message.body))); + } + + saveResult(key: string, value: string) { + sessionStorage.setItem(key, value); + } + + getSavedResult(key: string) { + return JSON.parse(sessionStorage.getItem(key)); + } + +} diff --git a/web/frontend/src/app/pages/generate-ddl/types.ts b/web/frontend/src/app/pages/generate-ddl/types.ts new file mode 100644 index 00000000..659650ef --- /dev/null +++ b/web/frontend/src/app/pages/generate-ddl/types.ts @@ -0,0 +1,15 @@ +export const generationStatuses = { + PLANNING_START: "Starting to plan the response", + PLANning_DONE: "Planning complete", + TAGS: "System processing/Configuration check", + PLANNING_FAILED: "Planning failed", + DOCS_RETRIEVED: "Information found", + ERROR: "An error occurred", + ATTEMPT_START: "Starting response generation", + PART: "Generating response", + ATTEMPT_COMPILE_OK: "Response successfully compiled", + FINAL_SUCCESS: "Response ready", + ATTEMPT_COMPILE_ERROR: "Error during compilation", + FINAL_FAILURE: "Generation failed", + CANCELLED: "Canceled" +} as const; \ No newline at end of file diff --git a/web/frontend/src/app/pages/query/query.component.ts b/web/frontend/src/app/pages/query/query.component.ts index c3c9c9ce..af128db3 100644 --- a/web/frontend/src/app/pages/query/query.component.ts +++ b/web/frontend/src/app/pages/query/query.component.ts @@ -69,7 +69,7 @@ import { LastQueriesService } from './services/last-queries.service'; import { QueryService } from './services/query.service'; import * as NotificationsActions from '../../core/modules/notifications/store/notifications.actions'; import { getAppInfo } from 'src/app/core/store/app/app.selectors'; -import { TimebaseService } from '../generate-ddl/generate-ddl.service'; +import { GenerateQueryService } from '../generate-ddl/generate-ddl.service'; import IRange = monaco.IRange; import { QqlEditorComponent } from 'src/app/shared/qql-editor/qql-editor.component'; @@ -162,7 +162,8 @@ export class QueryComponent implements OnInit, AfterViewInit { private permissionsService: PermissionsService, private shareLinkService: ShareLinkService, private gridTotalService: GridTotalService, - private timebaseService: TimebaseService + private generateQueryService: GenerateQueryService, + private elementRef: ElementRef ) {} ngOnInit() { @@ -199,9 +200,10 @@ export class QueryComponent implements OnInit, AfterViewInit { .pipe(takeUntil(this.destroy$)) .subscribe(); - if (this.timebaseService.currentQuery) { - this.form.patchValue({ query: this.timebaseService.currentQuery }); - this.timebaseService.currentQuery = null; + if (this.generateQueryService.currentQuery) { + this.form.patchValue({ query: this.generateQueryService.currentQuery }); + this.form.markAsPristine(); + this.generateQueryService.currentQuery = null; } this.liveGridName$ = this.tabId().pipe(map((id) => `gridLive${id}`)); diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-send-message/modal-send-message.component.ts b/web/frontend/src/app/pages/streams/components/modals/modal-send-message/modal-send-message.component.ts index 4d772e3e..d98a0a13 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-send-message/modal-send-message.component.ts +++ b/web/frontend/src/app/pages/streams/components/modals/modal-send-message/modal-send-message.component.ts @@ -29,6 +29,8 @@ import {FieldModel} from '../../../../../shared/utils/d import * as NotificationsActions from '../../../../../core/modules/notifications/store/notifications.actions'; import { getAppSettings } from 'src/app/core/store/app/app.selectors'; +const JSONbig = require('json-bigint')({ storeAsString: true }); + export interface editedMessageProps { symbols?: string[], types?: string[], @@ -452,7 +454,7 @@ export class ModalSendMessageComponent implements OnInit, AfterViewInit, OnDestr } saveJson(field: FieldModel) { - this.formGroup.get(field.name).patchValue(JSON.parse(this.jsonFieldControl.value)); + this.formGroup.get(field.name).patchValue(JSONbig.parse(this.jsonFieldControl.value)); this.jsonFieldControl.setValidators(null); this.jsonFieldControl.patchValue(null); this.editJsonField$.next(null); diff --git a/web/frontend/src/app/pages/streams/services/on-close-tab-alert.service.ts b/web/frontend/src/app/pages/streams/services/on-close-tab-alert.service.ts index a1e1576f..c8b7f573 100644 --- a/web/frontend/src/app/pages/streams/services/on-close-tab-alert.service.ts +++ b/web/frontend/src/app/pages/streams/services/on-close-tab-alert.service.ts @@ -28,10 +28,11 @@ export class OnCloseTabAlertService { check(tab: TabModel = null): Observable { return this.needAlertTabs().pipe( take(1), - switchMap((tabs) => { + switchMap(tabs => { const needToShow = (tab && tabs.some((t) => t.id === tab.id)) || (!tab && tabs.length); + const notificationMessageType = tabs[0]?.generateDDL ? 'onStopGenerationMessage' : 'onbeforeunloadMessage'; return needToShow && !this.withoutAlert - ? this.confirmModalService.confirm('notification_messages.onbeforeunloadMessage') + ? this.confirmModalService.confirm(`notification_messages.${notificationMessageType}`) : of(true); }), ); diff --git a/web/frontend/src/app/pages/streams/streams-routing.module.ts b/web/frontend/src/app/pages/streams/streams-routing.module.ts index f877a758..8efc7d93 100644 --- a/web/frontend/src/app/pages/streams/streams-routing.module.ts +++ b/web/frontend/src/app/pages/streams/streams-routing.module.ts @@ -80,6 +80,7 @@ const routes: Routes = [ generateDDL: true, }, canActivate: [ActiveTabGuard], + canDeactivate: [CheckShowingOnCloseAlertGuard], }, { path: 'chart', diff --git a/web/frontend/src/app/shared/live-grid/live-grid.component.ts b/web/frontend/src/app/shared/live-grid/live-grid.component.ts index 8e70a276..4a74118c 100644 --- a/web/frontend/src/app/shared/live-grid/live-grid.component.ts +++ b/web/frontend/src/app/shared/live-grid/live-grid.component.ts @@ -15,7 +15,7 @@ import { } from 'ag-grid-community'; import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { Observable, ReplaySubject, Subject, Subscription, of } from 'rxjs'; -import { filter, map, take, takeUntil, withLatestFrom, switchMap } from 'rxjs/operators'; +import { filter, map, take, takeUntil, withLatestFrom, switchMap, distinctUntilChanged } from 'rxjs/operators'; import { TabModel } from 'src/app/pages/streams/models/tab.model'; import { getActiveTab } from 'src/app/pages/streams/store/streams-tabs/streams-tabs.selectors'; import { WebsocketService } from '../../core/services/websocket.service'; @@ -188,6 +188,7 @@ export class LiveGridComponent implements OnInit, OnDestroy, OnChanges { this.appStore.pipe( select(getActiveTab), + distinctUntilChanged((t1, t2) => t1?.stream === t2?.stream), switchMap((tab: TabModel) => { if (tab?.stream?.endsWith('#topic#')) { return this.topicService.getTopicSchema(tab.stream.slice(0, tab.stream.length - 7)); diff --git a/web/frontend/src/app/shared/services/grid.service.ts b/web/frontend/src/app/shared/services/grid.service.ts index 41b27c36..e85bcf5d 100644 --- a/web/frontend/src/app/shared/services/grid.service.ts +++ b/web/frontend/src/app/shared/services/grid.service.ts @@ -369,7 +369,7 @@ export class GridService implements OnDestroy { return params.value.toFixed(exponent); } - if (['number', 'string'].includes(typeof params.value) && Math.abs(params.value) >= 1000) { + if (typeof params.value === 'number' && Math.abs(params.value) >= 1000 && Math.abs(params.value) <= Number.MAX_SAFE_INTEGER) { return parseFloat(params.value).toLocaleString(this.locale); } diff --git a/web/frontend/src/assets/i18n/en.json b/web/frontend/src/assets/i18n/en.json index 1a78c890..eb52409d 100644 --- a/web/frontend/src/assets/i18n/en.json +++ b/web/frontend/src/assets/i18n/en.json @@ -18,7 +18,8 @@ "updateMessageSucceeded": "Message is updated", "smallTimeRange": "You select too small time range. Time range should be more than {{range}}ms.", "auth_error_notification": "Your auth token it invalid. You will redirect to login page after closing this notification.", - "onbeforeunloadMessage": "There are some necessary changes on page. Do you really want to drop changes and continue your action?", + "onStopGenerationMessage": "Are you sure you want to leave the page and stop the response generation?", + "onbeforeunloadMessage": "Are you sure you want to leave the page and discard changes?", "isAbstractActivateError": "Impossible to make this class abstract because It's used in next fields:
{{classListString}}", "access_denied_page_text": "You have no access to this application.", "access_denied": "You have no permission to use next API:
"