diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebSocketConfig.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebSocketConfig.java index d09c2276..a7ce0043 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebSocketConfig.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/config/WebSocketConfig.java @@ -53,6 +53,7 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { public static final String SYSTEM_ENDPOINT = "/stomp/v0"; public static final String STREAMS_TOPIC = TOPIC + "/streams"; public static final String STRUCTURE_EVENTS_TOPIC = TOPIC + "/structure-events"; + public static final String TIMEBASE_STATUS_TOPIC = TOPIC + "/timebase-status"; public static final String FLOWCHART_TOPIC = TOPIC + "/flowchart"; public static final String FLOWCHART_METADATA_TOPIC = FLOWCHART_TOPIC + "/metadata"; public static final String FLOWCHART_RING_CENTER_TOPIC = FLOWCHART_TOPIC + "/ringCenter"; diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/GenAiController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/GenAiController.java index 80270f32..88c85e0c 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/GenAiController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/GenAiController.java @@ -34,11 +34,13 @@ @CrossOrigin public class GenAiController implements SubscriptionController { + private static final String TB_HEADER = "tbId"; + private final @Nullable GenAiService genAiService; - public GenAiController(SubscriptionControllerRegistry registry, + public GenAiController(SubscriptionControllerRegistry subscriptionRegistry, @Autowired(required = false) @Nullable GenAiService genAiService) { - registry.register(WebSocketConfig.GENAI_QQL_TOPIC, this); + subscriptionRegistry.register(WebSocketConfig.GENAI_QQL_TOPIC, this); this.genAiService = genAiService; } @@ -65,7 +67,8 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor headerAccessor, Subscr return () -> {}; } - genAiService.subscribe(username, userInput, rawStreamKeys, channel); + String tb = headerAccessor.getFirstNativeHeader(TB_HEADER); + genAiService.subscribe(username, userInput, rawStreamKeys, channel, tb); return () -> genAiService.unsubscribe(channel); } } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/GrafanaController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/GrafanaController.java index bb0afeae..03ff8016 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/GrafanaController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/GrafanaController.java @@ -60,8 +60,9 @@ public GrafanaVersion grafanaVersion() { @RequestMapping(value = "/streams", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity streams(@RequestParam(required = false, defaultValue = "") String template, @RequestParam(required = false, defaultValue = "0") int offset, - @RequestParam(required = false, defaultValue = "30") int limit) { - return ResponseEntity.ok(grafanaService.listStreams(template, offset, limit)); + @RequestParam(required = false, defaultValue = "30") int limit, + @RequestParam(required = false) String tb) { + return ResponseEntity.ok(grafanaService.listStreams(template, offset, limit, tb)); } @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @@ -69,15 +70,17 @@ public ResponseEntity streams(@RequestParam(required = false, defau public ResponseEntity symbols(@RequestParam String stream, @RequestParam(required = false, defaultValue = "") String template, @RequestParam(required = false, defaultValue = "0") int offset, - @RequestParam(required = false, defaultValue = "30") int limit) + @RequestParam(required = false, defaultValue = "30") int limit, + @RequestParam(required = false) String tb) throws NoSuchStreamException { - return ResponseEntity.ok(grafanaService.listSymbols(stream, template, offset, limit)); + return ResponseEntity.ok(grafanaService.listSymbols(stream, template, offset, limit, tb)); } @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/schema", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity schema(@RequestParam String stream) throws NoSuchStreamException { - return ResponseEntity.ok(grafanaService.schema(stream)); + public ResponseEntity schema(@RequestParam String stream, + @RequestParam(required = false) String tb) throws NoSuchStreamException { + return ResponseEntity.ok(grafanaService.schema(stream, tb)); } @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @@ -87,20 +90,23 @@ public List groupByOptions() { } @RequestMapping(value = "/queries/selectTS", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) - public List selectTS(@Valid @RequestBody DataQueryRequest request) throws ValidationException, RecordValidationException { - return grafanaService.timeSeries(request); + public List selectTS(@Valid @RequestBody DataQueryRequest request, + @RequestParam(required = false) String tb) throws ValidationException, RecordValidationException { + return grafanaService.timeSeries(request, tb); } @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/queries/selectDF", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) - public List selectDataFrame(@Valid @RequestBody DataQueryRequest request) throws ValidationException, RecordValidationException { - return grafanaService.dataFrames(request); + public List selectDataFrame(@Valid @RequestBody DataQueryRequest request, + @RequestParam(required = false) String tb) throws ValidationException, RecordValidationException { + return grafanaService.dataFrames(request, tb); } @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/queries/select", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) - public List select(@Valid @RequestBody DataQueryRequest request) throws ValidationException, RecordValidationException { - return grafanaService.select(request); + public List select(@Valid @RequestBody DataQueryRequest request, + @RequestParam(required = false) String tb) throws ValidationException, RecordValidationException { + return grafanaService.select(request, tb); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ImportCsvController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ImportCsvController.java index 96b7a22c..1d01c1c6 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ImportCsvController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ImportCsvController.java @@ -63,10 +63,11 @@ public ImportCsvController(SubscriptionControllerRegistry registry, CsvImportSer @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @PostMapping(value = "/csv/init") - public ResponseEntity initImport(@RequestParam String streamKey) throws UnknownStreamException { + public ResponseEntity initImport(@RequestParam String streamKey, + @RequestParam(required = false) String tb) throws UnknownStreamException { if (TextUtils.isEmpty(streamKey)) throw new UnknownStreamException(streamKey); - String id = importService.initImport(streamKey); + String id = importService.initImport(streamKey, tb); LOGGER.info().append("CSV Import initialization for stream ").append(streamKey) .append(". Import Id: ").append(id).commit(); return new ResponseEntity<>(Collections.singletonList(id), HttpStatus.CREATED); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ImportQsmsgController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ImportQsmsgController.java index 0d97b951..f5a1f039 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ImportQsmsgController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ImportQsmsgController.java @@ -70,7 +70,8 @@ public long initImport(@Valid @RequestBody ImportRequest importRequest) { importRequest.to != null ? importRequest.to.toEpochMilli() : Long.MAX_VALUE, getPeriodicity(importRequest.periodicity), importRequest.fileBySymbol, - importRequest.writeMode + importRequest.writeMode, + importRequest.tbId ) ); } 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 dfe4c7e1..1e6bf5b3 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 @@ -55,8 +55,9 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor headerAccessor, Subscr List symbols = headerAccessorHelper.getSymbols(headerAccessor); List types = headerAccessorHelper.getTypes(headerAccessor); JsonBigIntEncoding bigIntEncoding = HeaderAccessorHelper.getJsonBigIntEncoding(headerAccessor); + String tb = headerAccessor.getFirstNativeHeader("tbId"); - monitorService.subscribe(sessionId, subscriptionId, stream, null, fromTimestamp, types, symbols, channel::sendMessage, bigIntEncoding); + monitorService.subscribe(sessionId, subscriptionId, stream, null, fromTimestamp, types, symbols, channel::sendMessage, bigIntEncoding, tb); 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 b60ef833..e95e370e 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 @@ -56,7 +56,8 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor headerAccessor, Subscr List types = headerAccessorHelper.getTypes(headerAccessor); JsonBigIntEncoding bigIntEncoding = HeaderAccessorHelper.getJsonBigIntEncoding(headerAccessor); - monitorService.subscribe(sessionId, subscriptionId, null, qql, fromTimestamp, types, symbols, channel::sendMessage, bigIntEncoding); + String tbId = headerAccessor.getFirstNativeHeader("tbId"); + monitorService.subscribe(sessionId, subscriptionId, null, qql, fromTimestamp, types, symbols, channel::sendMessage, bigIntEncoding, tbId); return () -> monitorService.unsubscribe(sessionId, subscriptionId); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/OrderBookController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/OrderBookController.java index df9d7aa9..8783412a 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/OrderBookController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/OrderBookController.java @@ -48,14 +48,15 @@ public class OrderBookController implements SubscriptionController { private static final String SOURCE_HEADER = "source"; private static final String STREAMS_LIST_HEADER = "streams"; private static final String HIDDEN_EXCHANGES_LIST_HEADER = "hiddenExchanges"; + private static final String TB_HEADER = "tbId"; private final OrderBookService orderBookService; private final ObjectMapper objectMapper = new ObjectMapper(); @Autowired - public OrderBookController(SubscriptionControllerRegistry registry, OrderBookService orderBookService) { - registry.register(WebSocketConfig.ORDER_BOOK_TOPIC, this); + public OrderBookController(SubscriptionControllerRegistry subscriptionRegistry, OrderBookService orderBookService) { + subscriptionRegistry.register(WebSocketConfig.ORDER_BOOK_TOPIC, this); this.orderBookService = orderBookService; } @@ -67,7 +68,7 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor headerAccessor, Subscr } private OrderBookSubscriptionOptions getSubscriptionOptions(SimpMessageHeaderAccessor headerAccessor, - SubscriptionChannel channel){ + SubscriptionChannel channel) { String instrument = headerAccessor.getFirstNativeHeader(INSTRUMENT_HEADER); if (instrument == null || instrument.isEmpty()) { throw new IllegalArgumentException("Unknown instrument, specify '" + INSTRUMENT_HEADER + "' STOMP header."); @@ -115,8 +116,10 @@ private Subscription subscribe(SimpMessageHeaderAccessor headerAccessor, Subscri .append("; SessionId: ").append(sessionId) .append("; SubscriptionId: ").append(subscriptionId).commit(); + String tb = headerAccessor.getFirstNativeHeader(TB_HEADER); orderBookService.subscribe( sessionId, subscriptionId, so.getInstrument(), so.getStreams(), so.getHiddenExchanges(), + tb, (l2PackageDto) -> { if (LOG.isTraceEnabled()) { LOG.trace().append("Sending L2PackageDto for instrument ").append(so.getInstrument()) diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/PlaybackController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/PlaybackController.java index 74cb9c2b..9acf98d7 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/PlaybackController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/PlaybackController.java @@ -50,8 +50,9 @@ public class PlaybackController implements SubscriptionController { private final PlaybackServiceImpl playbackService; @Autowired - public PlaybackController(SubscriptionControllerRegistry registry, PlaybackServiceImpl playbackService) { - registry.register(WebSocketConfig.PLAYBACK_TOPIC, this); + public PlaybackController(SubscriptionControllerRegistry subscriptionRegistry, + PlaybackServiceImpl playbackService) { + subscriptionRegistry.register(WebSocketConfig.PLAYBACK_TOPIC, this); this.playbackService = playbackService; } @@ -70,6 +71,8 @@ public ResponseEntity createPlayback(@RequestBody PlaybackRequest request, config.setCyclic(request.isCyclic()); config.setTargetTopic(request.isTargetTopic()); config.setPermanent(request.isPermanent()); + config.setSourceTb(request.getSourceTb()); + config.setTargetTb(request.getTargetTb()); return new ResponseEntity<>(playbackService.createPlayer(config, user.getName()), HttpStatus.CREATED); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimeBaseTreeController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimeBaseTreeController.java index 51050ef4..21e65159 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimeBaseTreeController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/TimeBaseTreeController.java @@ -49,9 +49,13 @@ public ResponseEntity tree(@RequestBody TimeBaseStructureRequestDef @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{stream}/{symbol}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getSymbolTree(@PathVariable String stream, @PathVariable String symbol, @RequestBody TimeBaseStructureRequestDef request) { + public ResponseEntity getSymbolTree( + @PathVariable String stream, + @PathVariable String symbol, + @RequestParam(required = false) String tb, + @RequestBody TimeBaseStructureRequestDef request) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body( - timeBaseTree.findSymbolTree(stream, symbol, request.isShowSpaces(), request.isViews()) + timeBaseTree.findSymbolTree(tb, stream, symbol, request.isShowSpaces(), request.isViews()) ); } 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 21c6547e..76931cac 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 @@ -58,8 +58,10 @@ import com.epam.deltix.tbwg.webapp.model.smd.InstrumentDef; //import com.epam.deltix.tbwg.webapp.services.InstrumentsService; import com.epam.deltix.tbwg.webapp.services.OptionsService; +import com.epam.deltix.tbwg.webapp.model.TimebaseInstanceDef; import com.epam.deltix.tbwg.webapp.services.orderbook.OrderBookDebugger; import com.epam.deltix.tbwg.webapp.services.orderbook.OrderBookSnapshotRequest; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.timebase.base.SchemaManipulationService; import com.epam.deltix.tbwg.webapp.services.timebase.base.SelectService; @@ -124,7 +126,7 @@ public class TimebaseController { private static final Log LOGGER = LogFactory.getLog(TimebaseController.class); - private final TimebaseService service; + private final TimebaseRegistry registry; private final SchemaManipulationService schemaManipulationService; private final SelectService selectService; //private final InstrumentsService instrumentsService; @@ -136,10 +138,11 @@ public class TimebaseController { private final AtomicLong idGenerator = new AtomicLong(System.currentTimeMillis()); @Autowired - public TimebaseController(TimebaseService service, SelectService selectService, ExportService exportService, + public TimebaseController(TimebaseRegistry registry, + SelectService selectService, ExportService exportService, SchemaManipulationService schemaManipulationService, OptionsService optionsService, ViewService viewService, OrderBookDebugger orderBookDebugger) { - this.service = service; + this.registry = registry; this.schemaManipulationService = schemaManipulationService; this.selectService = selectService; //this.instrumentsService = instrumentsService; @@ -152,8 +155,10 @@ public TimebaseController(TimebaseService service, SelectService selectService, @RequestMapping(value = {"/v", "/"}, method = {RequestMethod.GET, RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public VersionDef version() { - return new VersionDef("Timebase Web Gateway", Application.VERSION, System.currentTimeMillis(), - new VersionDef.TimeBase(Version.getVersion(), service.getServerVersion(), service.isConnected()), true); + List timebases = registry.getAll().stream() + .map(TimebaseInstanceDef::new) + .collect(Collectors.toList()); + return new VersionDef("Timebase Web Gateway", Application.VERSION, System.currentTimeMillis(), timebases, true); } @RequestMapping(value = {"/correlationId"}, method = RequestMethod.GET) @@ -162,6 +167,15 @@ public long correlationId() { return idGenerator.incrementAndGet(); } + @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") + @RequestMapping(value = "/timebases", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> timebases() { + List result = registry.getAll().stream() + .map(TimebaseInstanceDef::new) + .collect(Collectors.toList()); + return ResponseEntity.ok(result); + } + /** *

Returns data from the specified streams, according to the specified options. The messages * are returned from the cursor strictly ordered by time. Within the same @@ -177,13 +191,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, - JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { + JsonBigIntEncoding bigIntEncoding, + @RequestParam(required = false) String tb) 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, bigIntEncoding)); + .body(selectService.select(registry.resolve(tb), select, MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET, bigIntEncoding)); } /** @@ -219,6 +234,7 @@ public ResponseEntity select( @RequestParam(required = false) Integer rows, @RequestParam(required = false) String space, @RequestParam(required = false) boolean reverse, + @RequestParam(required = false) String tb, JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { SelectRequest request = new SelectRequest(); request.streams = streams; @@ -232,7 +248,7 @@ public ResponseEntity select( request.reverse = reverse; request.depth = depth; request.space = space; - return select(request, bigIntEncoding); + return select(request, bigIntEncoding, tb); } /** @@ -252,13 +268,14 @@ public ResponseEntity select( @RequestMapping(value = "/{streamId}/select", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity select(@PathVariable String streamId, @Valid @RequestBody(required = false) StreamRequest select, + @RequestParam(required = false) String tb, 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, bigIntEncoding); + select.rows, select.space, select.reverse, tb, bigIntEncoding); } /** @@ -301,11 +318,12 @@ public ResponseEntity select( @RequestParam(required = false) Integer rows, @RequestParam(required = false) String space, @RequestParam(required = false) boolean reverse, + @RequestParam(required = false) String tb, 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, bigIntEncoding); + return select(new String[]{streamId}, symbols, types, depth, from, to, offset, rows, space, reverse, tb, bigIntEncoding); } // download operation is permitted for any user @@ -331,7 +349,8 @@ public ResponseEntity download(@RequestParam(required = t @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/export", method = {RequestMethod.POST}) - public ResponseEntity export(@Valid @RequestBody(required = false) ExportStreamsRequest select) + public ResponseEntity export(@Valid @RequestBody(required = false) ExportStreamsRequest select, + @RequestParam(required = false) String tb) throws NoStreamsException { if (select == null) select = new ExportStreamsRequest(); @@ -339,9 +358,10 @@ public ResponseEntity export(@Valid @RequestBody(required = false) E if (select.streams == null) throw new NoStreamsException(); + TimebaseService tbService = registry.resolve(tb); ArrayList streams = new ArrayList<>(); for (String streamId : select.streams) { - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = tbService.getStream(streamId); if (stream != null) streams.add(stream); } @@ -382,7 +402,7 @@ public ResponseEntity export(@Valid @RequestBody(required = false) E return ResponseEntity.ok(new DownloadId( exportService.prepareExport( - new StreamsExportSourceFactory(service, startTime, options, tickStreams, select.getTypes(), ids), + new StreamsExportSourceFactory(tbService, startTime, options, tickStreams, select.getTypes(), ids), select, startTime, select.getEndTime(), startIndex, endIndex, periodicity, descriptors ) )); @@ -402,7 +422,8 @@ public ResponseEntity export( @RequestParam(required = false) boolean skipEmpty, @RequestParam(required = false, defaultValue = "true") boolean enableStaticFields, @RequestParam(required = false) String datetimeFormat, - @RequestParam(required = false) boolean reverse) throws NoStreamsException { + @RequestParam(required = false) boolean reverse, + @RequestParam(required = false) String tb) throws NoStreamsException { ExportStreamsRequest request = new ExportStreamsRequest(); request.streams = streams; @@ -417,14 +438,15 @@ public ResponseEntity export( request.enableStaticFields = enableStaticFields; request.datetimeFormat = datetimeFormat; - return export(request); + return export(request, tb); } @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/export", method = RequestMethod.POST) - public ResponseEntity export(@PathVariable String streamId, @Valid @RequestBody(required = false) ExportRequest select) + public ResponseEntity export(@PathVariable String streamId, @Valid @RequestBody(required = false) ExportRequest select, + @RequestParam(required = false) String tb) throws UnknownStreamException { - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = registry.resolve(tb).getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -445,7 +467,7 @@ public ResponseEntity export(@PathVariable String streamId, @Valid @ return ResponseEntity.ok(new DownloadId( exportService.prepareExport( - new StreamsExportSourceFactory(service, startTime, options, new DXTickStream[]{stream}, select.getTypes(), ids), + new StreamsExportSourceFactory(registry.resolve(tb), startTime, options, new DXTickStream[]{stream}, select.getTypes(), ids), select, startTime, select.getEndTime(), startIndex, endIndex, periodicity, stream.getTypes() ) @@ -465,7 +487,8 @@ public ResponseEntity export( @RequestParam(required = false) boolean skipEmpty, @RequestParam(required = false, defaultValue = "true") boolean enableStaticFields, @RequestParam(required = false) String datetimeFormat, - @RequestParam(required = false) boolean reverse) throws NoStreamsException, UnknownStreamException { + @RequestParam(required = false) boolean reverse, + @RequestParam(required = false) String tb) throws NoStreamsException, UnknownStreamException { if (TextUtils.isEmpty(streamId)) throw new NoStreamsException(); @@ -482,12 +505,13 @@ public ResponseEntity export( request.enableStaticFields = enableStaticFields; request.datetimeFormat = datetimeFormat; - return export(streamId, request); + return export(streamId, request, tb); } @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/export-query", method = {RequestMethod.POST}) - public ResponseEntity exportQuery(@Valid @RequestBody(required = false) QueryRequest query) + public ResponseEntity exportQuery(@Valid @RequestBody(required = false) QueryRequest query, + @RequestParam(required = false) String tb) throws InvalidQueryException { if (query == null || StringUtils.isEmpty(query.query)) { @@ -501,7 +525,8 @@ public ResponseEntity exportQuery(@Valid @RequestBody(required = fal ExportRequest request = new ExportRequest(); request.format = query.format != null ? query.format : ExportFormat.QSMSG; - ClassSet classSet = service.getConnection().describeQuery(query.query, options); + TimebaseService tbService = registry.resolve(tb); + ClassSet classSet = tbService.getConnection().describeQuery(query.query, options); ClassDescriptor[] descriptors = classSet.getContentClasses(); RecordClassDescriptor[] rcds = Arrays.stream(descriptors) @@ -513,7 +538,7 @@ public ResponseEntity exportQuery(@Valid @RequestBody(required = fal return ResponseEntity.ok(new DownloadId( exportService.prepareExport( - new QueryExportSourceFactory(service, options, query.query), + new QueryExportSourceFactory(tbService, options, query.query), request, Long.MIN_VALUE, Long.MAX_VALUE, 0, -1, null, rcds ) )); @@ -527,8 +552,9 @@ public ResponseEntity exportQuery(@Valid @RequestBody(required = fal */ @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/describe", method = {RequestMethod.GET}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity describeStream(@PathVariable String streamId) throws UnknownStreamException { - return ResponseEntity.ok(schemaManipulationService.describeStream(streamId)); + public ResponseEntity describeStream(@PathVariable String streamId, + @RequestParam(required = false) String tb) throws UnknownStreamException { + return ResponseEntity.ok(schemaManipulationService.describeStream(registry.resolve(tb), streamId)); } /** @@ -539,7 +565,9 @@ public ResponseEntity describeStream(@PathVariable String stre */ @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/spaces", method = {RequestMethod.GET}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity listSpaces(@PathVariable String streamId, @RequestParam(required = false, defaultValue = "") String filter) + public ResponseEntity listSpaces(@PathVariable String streamId, + @RequestParam(required = false, defaultValue = "") String filter, + @RequestParam(required = false) String tb) throws UnknownStreamException { LOGGER.log(LogLevel.INFO, "GET [%s].listSpaces(filter = %s)").with(streamId).with(filter); @@ -547,7 +575,7 @@ public ResponseEntity listSpaces(@PathVariable String streamId, @RequestParam if (TextUtils.isEmpty(streamId)) throw new UnknownStreamException(streamId); - DXTickStream stream = service.getStreamChecked(streamId); + DXTickStream stream = registry.resolve(tb).getStreamChecked(streamId); String[] spaces = stream != null ? stream.listSpaces() : EMPTY_LIST; if (spaces != null) { @@ -569,12 +597,14 @@ public ResponseEntity listSpaces(@PathVariable String streamId, @RequestParam */ @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/purge", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity purge(@PathVariable String streamId, @RequestBody(required = true) SimpleRequest request) throws UnknownStreamException { - ResponseEntity entity = checkWritable("Purge stream [" + streamId + "] Failed"); + public ResponseEntity purge(@PathVariable String streamId, @RequestBody(required = true) SimpleRequest request, + @RequestParam(required = false) String tb) throws UnknownStreamException { + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Purge stream [" + streamId + "] Failed"); if (entity != null) return entity; - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -594,13 +624,15 @@ public ResponseEntity purge(@PathVariable String streamId, @RequestBody(requi */ @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/delete", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity delete(@PathVariable String streamId) + public ResponseEntity delete(@PathVariable String streamId, + @RequestParam(required = false) String tb) throws UnknownStreamException { - ResponseEntity entity = checkWritable("Delete stream [" + streamId + "] Failed"); + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Delete stream [" + streamId + "] Failed"); if (entity != null) return entity; - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -619,13 +651,15 @@ public ResponseEntity delete(@PathVariable String streamId) @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/rename", method = RequestMethod.POST, headers = "Content-Type=multipart/form-data", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity renameStream(@PathVariable String streamId, @RequestParam String newStreamId) + public ResponseEntity renameStream(@PathVariable String streamId, @RequestParam String newStreamId, + @RequestParam(required = false) String tb) throws UnknownStreamException { - ResponseEntity entity = checkWritable("Rename stream [" + streamId + "] Failed"); + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Rename stream [" + streamId + "] Failed"); if (entity != null) return entity; - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -650,12 +684,14 @@ public ResponseEntity renameStream(@PathVariable String streamId, @RequestPar @RequestMapping(value = "/{streamId}/{symbol}/rename", method = RequestMethod.POST, headers = "Content-Type=multipart/form-data", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity renameSymbol(@PathVariable String streamId, @PathVariable String symbol, - @RequestParam String newSymbol) throws UnknownStreamException { - ResponseEntity entity = checkWritable("Rename stream [" + streamId + "] Failed"); - if (entity != null) + @RequestParam String newSymbol, + @RequestParam(required = false) String tb) throws UnknownStreamException { + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Rename stream [" + streamId + "] Failed"); + if (entity != null) { return entity; - - DXTickStream stream = service.getStream(streamId); + } + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -687,12 +723,14 @@ public ResponseEntity renameSymbol(@PathVariable String streamId, @PathVariab @RequestMapping(value = "/{streamId}/renameSpace", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity renameSpace(@PathVariable String streamId, @RequestParam String space, - @RequestParam String newName) throws UnknownStreamException { - ResponseEntity entity = checkWritable("Rename stream [" + streamId + "] Failed"); - if (entity != null) + @RequestParam String newName, + @RequestParam(required = false) String tb) throws UnknownStreamException { + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Rename stream [" + streamId + "] Failed"); + if (entity != null) { return entity; - - DXTickStream stream = service.getStream(streamId); + } + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -711,13 +749,15 @@ public ResponseEntity renameSpace(@PathVariable String streamId, @RequestPara @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/deleteSpace", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity deleteSpace(@PathVariable String streamId, @RequestParam String space) + public ResponseEntity deleteSpace(@PathVariable String streamId, @RequestParam String space, + @RequestParam(required = false) String tb) throws UnknownStreamException { - ResponseEntity entity = checkWritable("Delete stream [" + streamId + "] space [" + space + "] failed"); + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Delete stream [" + streamId + "] space [" + space + "] failed"); if (entity != null) return entity; - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -735,14 +775,15 @@ public ResponseEntity deleteSpace(@PathVariable String streamId, @RequestPara */ @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/deleteSymbols", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity deleteSymbols(@PathVariable String streamId, @RequestBody String[] symbols) + public ResponseEntity deleteSymbols(@PathVariable String streamId, @RequestBody String[] symbols, + @RequestParam(required = false) String tb) throws UnknownStreamException { - - ResponseEntity entity = checkWritable("Delete stream [" + streamId + "] symbols " + Arrays.toString(symbols) + " failed"); + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Delete stream [" + streamId + "] symbols " + Arrays.toString(symbols) + " failed"); if (entity != null) return entity; - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -763,15 +804,16 @@ public ResponseEntity deleteSymbols(@PathVariable String streamId, @RequestBo @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/setPeriodicity", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity setPeriodicity(@PathVariable String streamId, @RequestParam String periodicity) + public ResponseEntity setPeriodicity(@PathVariable String streamId, @RequestParam String periodicity, + @RequestParam(required = false) String tb) throws UnknownStreamException { - ResponseEntity entity = checkWritable("Change periodicity of stream [" + streamId + "] failed"); + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Change periodicity of stream [" + streamId + "] failed"); if (entity != null) return entity; - DXTickStream stream = service.getStream(streamId); - + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -790,13 +832,14 @@ public ResponseEntity setPeriodicity(@PathVariable String streamId, @RequestP @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/truncate", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity truncate(@PathVariable String streamId, @RequestBody(required = true) SimpleRequest request, - OutputStream outputStream) throws UnknownStreamException { - - ResponseEntity entity = checkWritable("Truncate stream [" + streamId + "] Failed"); + OutputStream outputStream, + @RequestParam(required = false) String tb) throws UnknownStreamException { + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Truncate stream [" + streamId + "] Failed"); if (entity != null) return entity; - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -829,13 +872,15 @@ public ResponseEntity truncate(@PathVariable String streamId, @RequestBody(re public ResponseEntity write(@PathVariable String streamId, @RequestParam(required = false) String space, @RequestParam(required = false, defaultValue = "APPEND") LoadingOptions.WriteMode writeMode, + @RequestParam(required = false) String tb, @RequestBody String messages) throws UnknownStreamException { - ResponseEntity entity = checkWritable("Write failed."); + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Write failed."); if (entity != null) return entity; - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -884,14 +929,15 @@ public synchronized ResponseEntity edit(@PathVariable Str @RequestParam Instant timestamp, @RequestParam Integer offset, @RequestParam boolean reverse, + @RequestParam(required = false) String tb, @RequestBody String message) throws UnknownStreamException { - ResponseEntity entity = checkWritable("Write failed."); - if (entity != null) { + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Write failed."); + if (entity != null) return entity; - } - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = tbService.getStream(streamId); if (stream == null) { throw new UnknownStreamException(streamId); } @@ -998,7 +1044,7 @@ private ErrorWriter insertMessages(DXTickStream stream, List message return listener; } - ResponseEntity checkWritable(String error) { + ResponseEntity checkWritable(TimebaseService service, String error) { if (service.isReadonly()) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(outputStream -> { @@ -1029,8 +1075,9 @@ 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, JsonBigIntEncoding bigIntEncoding) { - DXTickStream stream = service.getStream(streamId); + OutputStream outputStream, JsonBigIntEncoding bigIntEncoding, + @RequestParam(required = false) String tb) { + DXTickStream stream = registry.resolve(tb).getStream(streamId); if (stream == null) //noinspection unchecked @@ -1095,6 +1142,7 @@ public ResponseEntity select( @RequestParam(required = false) Integer rows, @RequestParam(required = false) String space, @RequestParam(required = false) boolean reverse, + @RequestParam(required = false) String tb, JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { if (TextUtils.isEmpty(streamId)) throw new NoStreamsException(); @@ -1102,7 +1150,7 @@ public ResponseEntity select( if (TextUtils.isEmpty(symbolId)) return ResponseEntity.notFound().build(); - return select(new String[]{streamId}, new String[]{symbolId}, types, depth, from, to, offset, rows, space, reverse, bigIntEncoding); + return select(new String[]{streamId}, new String[]{symbolId}, types, depth, from, to, offset, rows, space, reverse, tb, bigIntEncoding); } private SelectionOptions getSelectionOption(BaseRequest r) { @@ -1145,11 +1193,12 @@ public ResponseEntity allTypes() { @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/createStream", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity createStream(@RequestParam String key, - @RequestBody SchemaDef schema) throws WriteOperationsException { + @RequestBody SchemaDef schema, + @RequestParam(required = false) String tb) throws WriteOperationsException { TBWGUtils.validateStreamKey(key); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) - .body(schemaManipulationService.createStream(key, schema)); + .body(schemaManipulationService.createStream(registry.resolve(tb), key, schema)); } @ResponseBody @@ -1173,11 +1222,12 @@ public boolean validateStreamKey(@RequestParam String key) { @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/getSchemaChanges", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getSchemaChanges(@PathVariable String streamId, - @RequestBody SchemaChangesRequest schemaChangesRequest) + @RequestBody SchemaChangesRequest schemaChangesRequest, + @RequestParam(required = false) String tb) throws UnknownStreamException { return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) - .body(schemaManipulationService.schemaChanges(streamId, schemaChangesRequest)); + .body(schemaManipulationService.schemaChanges(registry.resolve(tb), streamId, schemaChangesRequest)); } /** @@ -1192,11 +1242,12 @@ public ResponseEntity getSchemaChanges(@PathVariable St */ @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/changeSchema", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity changeSchema(@PathVariable String streamId, @RequestBody ChangeSchemaRequest changeSchemaRequest) + public ResponseEntity changeSchema(@PathVariable String streamId, @RequestBody ChangeSchemaRequest changeSchemaRequest, + @RequestParam(required = false) String tb) throws InvalidSchemaChangeException, UnknownStreamException, WriteOperationsException { return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) - .body(schemaManipulationService.changeSchema(streamId, changeSchemaRequest)); + .body(schemaManipulationService.changeSchema(registry.resolve(tb), streamId, changeSchemaRequest)); } @@ -1209,11 +1260,12 @@ public ResponseEntity changeSchema(@PathVariable String streamId, @Re @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/schema", method = {RequestMethod.GET}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity schema(@PathVariable String streamId, - @RequestParam(required = false, defaultValue = "false") boolean tree) + @RequestParam(required = false, defaultValue = "false") boolean tree, + @RequestParam(required = false) String tb) throws UnknownStreamException { return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) - .body(schemaManipulationService.schema(streamId, tree)); + .body(schemaManipulationService.schema(registry.resolve(tb), streamId, tree)); } /** @@ -1277,7 +1329,8 @@ public ResponseEntity settings(OutputStream outputStream) { LOGGER.log(LogLevel.INFO, "GET App Settings"); AppSettingDef def = new AppSettingDef(); - def.hasNanoseconds = VersionUtils.versionHasNsEncoding(service.getServerVersion()); + def.hasNanoseconds = registry.getAll().stream() + .allMatch(svc -> VersionUtils.versionHasNsEncoding(svc.getServerVersion())); return ResponseEntity.ok(def); } @@ -1291,14 +1344,15 @@ public ResponseEntity settings(OutputStream outputStream) { @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/describe", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity describe(@Valid @RequestBody QueryRequest select, - @RequestParam(required = false, defaultValue = "false") boolean tree) { + @RequestParam(required = false, defaultValue = "false") boolean tree, + @RequestParam(required = false) String tb) { if (select == null || StringUtils.isEmpty(select.query)) return ResponseEntity.badRequest().build(); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) - .body(schemaManipulationService.describe(select, tree)); + .body(schemaManipulationService.describe(registry.resolve(tb), select, tree)); } /** @@ -1309,12 +1363,13 @@ public ResponseEntity describe(@Valid @RequestBody QueryRequest selec */ @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/compileQuery", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity compile(@Valid @RequestBody QueryRequest select) { + public ResponseEntity compile(@Valid @RequestBody QueryRequest select, + @RequestParam(required = false) String tb) { if (select == null || StringUtils.isEmpty(select.query)) return ResponseEntity.badRequest().build(); - DXTickDB connection = service.getConnection(); + DXTickDB connection = registry.resolve(tb).getConnection(); if (connection instanceof TickDBClient) { ArrayList tokens = new ArrayList(); @@ -1335,8 +1390,9 @@ public ResponseEntity compile(@Valid @RequestBody QueryRequest se @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/options", method = {RequestMethod.GET}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity streamOptions(@PathVariable String streamId) throws UnknownStreamException { - DXTickStream stream = service.getStream(streamId); + public ResponseEntity streamOptions(@PathVariable String streamId, + @RequestParam(required = false) String tb) throws UnknownStreamException { + DXTickStream stream = registry.resolve(tb).getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -1346,8 +1402,9 @@ public ResponseEntity streamOptions(@PathVariable String strea @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @PutMapping(value = "/{streamId}/options", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity updateOptions(@PathVariable String streamId, @RequestBody StreamOptionsDef options) throws UnknownStreamException { - DXTickStream stream = service.getStream(streamId); + public ResponseEntity updateOptions(@PathVariable String streamId, @RequestBody StreamOptionsDef options, + @RequestParam(required = false) String tb) throws UnknownStreamException { + DXTickStream stream = registry.resolve(tb).getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -1358,8 +1415,9 @@ public ResponseEntity updateOptions(@PathVariable String strea @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/options/{symbolId}", method = {RequestMethod.GET}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity symbolOptions(@PathVariable String streamId, - @PathVariable String symbolId) throws UnknownStreamException { - DXTickStream stream = service.getStream(streamId); + @PathVariable String symbolId, + @RequestParam(required = false) String tb) throws UnknownStreamException { + DXTickStream stream = registry.resolve(tb).getStream(streamId); if (stream == null) { throw new UnknownStreamException(streamId); @@ -1372,8 +1430,9 @@ public ResponseEntity symbolOptions(@PathVariable String streamId, @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/options/backgroundTask", method = {RequestMethod.GET}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getBackgroundTaskInfo(@PathVariable String streamId) throws UnknownStreamException { - DXTickStream stream = service.getStream(streamId); + public ResponseEntity getBackgroundTaskInfo(@PathVariable String streamId, + @RequestParam(required = false) String tb) throws UnknownStreamException { + DXTickStream stream = registry.resolve(tb).getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -1383,13 +1442,14 @@ public ResponseEntity getBackgroundTaskInfo(@PathVariable Str @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/{streamId}/abortBackgroundTask", method = {RequestMethod.GET}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity abortBackgroundProcess(@PathVariable String streamId) throws UnknownStreamException { - - ResponseEntity entity = checkWritable("Abort background task for stream [" + streamId + "] Failed"); + public ResponseEntity abortBackgroundProcess(@PathVariable String streamId, + @RequestParam(required = false) String tb) throws UnknownStreamException { + TimebaseService tbService = registry.resolve(tb); + ResponseEntity entity = checkWritable(tbService, "Abort background task for stream [" + streamId + "] Failed"); if (entity != null) return entity; - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = tbService.getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -1447,8 +1507,9 @@ private static String getTypeName(DataType type) { public ResponseEntity range(@PathVariable String streamId, @RequestParam(value = "symbols", required = false) String[] symbols, @RequestParam(required = false) String space, - @RequestParam(required = false) Long barSize) throws UnknownStreamException { - DXTickStream stream = service.getStream(streamId); + @RequestParam(required = false) Long barSize, + @RequestParam(required = false) String tb) throws UnknownStreamException { + DXTickStream stream = registry.resolve(tb).getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -1481,9 +1542,10 @@ public ResponseEntity range(@PathVariable String streamId, @RequestMapping(value = "/{streamId}/symbols", method = {RequestMethod.GET}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity symbols(@PathVariable String streamId, @RequestParam(required = false, defaultValue = "") String filter, - @RequestParam(required = false) String space) throws UnknownStreamException { + @RequestParam(required = false) String space, + @RequestParam(required = false) String tb) throws UnknownStreamException { - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = registry.resolve(tb).getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -1507,29 +1569,42 @@ public ResponseEntity symbols(@PathVariable String streamId, @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/streams", method = RequestMethod.GET) public ResponseEntity streams(@RequestParam(required = false, defaultValue = "") String filter, - @RequestParam(required = false) boolean spaces) { + @RequestParam(required = false) boolean spaces, + @RequestParam(required = false) String tb) { LOGGER.log(LogLevel.INFO, "GET streams() using filter = %s").with(filter); - DXTickStream[] streams = Arrays.stream(service.listStreams(filter, spaces)) - .filter(stream -> !viewService.isViewStream(stream.getKey())).toArray(DXTickStream[]::new); - -// List list = Arrays.stream(streams) -// //.filter((stream)->stream.getScope() == StreamScope.DURABLE) // Hide 'transient' streams -// .filter((s) -> !s.getKey().contains("#")) // Hide 'system' streams -// .collect(Collectors.toList()); + boolean explicitTb = tb != null && !tb.isEmpty(); + List services = explicitTb + ? Collections.singletonList(registry.resolve(tb)) + : registry.getAll(); - StreamDef[] result = new StreamDef[streams.length]; - - for (int i = 0, listSize = streams.length; i < listSize; i++) { - DXTickStream stream = streams[i]; - result[i] = new StreamDef(stream.getKey(), stream.getName(), stream.listEntities().length); - - ChartTypeDef[] chartTypes = TBWGUtils.chartTypes(stream); - if (chartTypes.length > 0) - result[i].chartType = chartTypes; + List result = new ArrayList<>(); + for (TimebaseService svc : services) { + DXTickStream[] streams; + try { + streams = Arrays.stream(svc.listStreams(filter, spaces)) + .filter(stream -> !viewService.isViewStream(stream.getKey())).toArray(DXTickStream[]::new); + } catch (Exception e) { + // A single unreachable timebase must not prevent listing streams from the others. + // If the caller explicitly asked for this timebase (tb param), let the error propagate. + if (explicitTb) { + throw e; + } + LOGGER.warn().append("Timebase [").append(svc.getId()).append("] is unavailable, skipping it in streams() : ") + .append(e.getMessage()).commit(); + continue; + } + for (DXTickStream stream : streams) { + StreamDef def = new StreamDef(stream.getKey(), stream.getName(), stream.listEntities().length); + def.tbId = svc.getId(); + ChartTypeDef[] chartTypes = TBWGUtils.chartTypes(stream); + if (chartTypes.length > 0) + def.chartType = chartTypes; + result.add(def); + } } - return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result.toArray(new StreamDef[0])); } /** @@ -1541,11 +1616,13 @@ 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, + @RequestParam(required = false) String tb, 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"))) + TimebaseService tbService = registry.resolve(tb); + if (tbService.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")) { @@ -1561,7 +1638,7 @@ public ResponseEntity query(Principal principal, @Valid @ return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) .body(new MessageSource2ResponseStream( - service.getConnection().executeQuery( + tbService.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, bigIntEncoding)); } @@ -1572,12 +1649,14 @@ public ResponseEntity query(Principal principal, @Valid @ @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, + @RequestParam(required = false) String tb, 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"))) + TimebaseService tbService = registry.resolve(tb); + if (tbService.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."); @@ -1588,7 +1667,7 @@ public ResponseEntity unlimitedQuery(Principal principal, return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) .body(new MessageSource2ResponseStream( - service.getConnection().executeQuery( + tbService.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)); } @@ -1605,12 +1684,12 @@ private boolean isDdlQuery(String query) { @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/query-info/functions", method = {RequestMethod.GET}) - public List queryFunctions() { + public List queryFunctions(@RequestParam(required = false) String tb) { SelectionOptions options = new SelectionOptions(); options.raw = true; options.live = false; - try (InstrumentMessageSource cursor = service.getConnection().executeQuery( + try (InstrumentMessageSource cursor = registry.resolve(tb).getConnection().executeQuery( "select\n" + "stateful.id as 'name',\n" + "stateful.returnType as 'returnType',\n" + @@ -1645,12 +1724,12 @@ public List queryFunctions() { @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/query-info/functions-short", method = {RequestMethod.GET}) - public Set queryFunctionsShort() { + public Set queryFunctionsShort(@RequestParam(required = false) String tb) { SelectionOptions options = new SelectionOptions(); options.raw = true; options.live = false; - try (InstrumentMessageSource cursor = service.getConnection().executeQuery( + try (InstrumentMessageSource cursor = registry.resolve(tb).getConnection().executeQuery( "select stateful.id as 'name', (size([1]) == 1) as 'isStateful' ARRAY JOIN stateful_functions() as 'stateful'\n" + "UNION \n" + "select stateless.id as 'name', (size([1]) == 0) as 'isStateful' ARRAY JOIN stateless_functions() as 'stateless'", @@ -1695,8 +1774,9 @@ public Set queryFunctionsShort() { @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, + @RequestParam(required = false) String tb, JsonBigIntEncoding bigIntEncoding) throws UnknownStreamException { - DXTickStream stream = service.getStream(streamId); + DXTickStream stream = registry.resolve(tb).getStream(streamId); if (stream == null) throw new UnknownStreamException(streamId); @@ -1726,7 +1806,7 @@ public ResponseEntity filter(@PathVariable String streamI return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) - .body(new MessageSource2ResponseStream(service.getConnection() + .body(new MessageSource2ResponseStream(registry.resolve(tb).getConnection() .executeQuery(query, options, null, null, startTime), endTime, startIndex, endIndex, MAX_NUMBER_OF_RECORDS_PER_REST_RESULTSET, bigIntEncoding)); } @@ -1753,7 +1833,8 @@ public ResponseEntity orderBook( @RequestParam(required = false) Long offset, @RequestParam(required = false) String space, @RequestParam(required = false) boolean reverse, - @RequestParam(defaultValue = "L2") ModelDataSourceType source) throws NoStreamsException + @RequestParam(defaultValue = "L2") ModelDataSourceType source, + @RequestParam(required = false) String tb) throws NoStreamsException { OrderBookSnapshotRequest request = new OrderBookSnapshotRequest(); request.setStreams(streams); @@ -1768,12 +1849,13 @@ public ResponseEntity orderBook( return ResponseEntity.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) - .body(orderBookDebugger.snapshot(request)); + .body(orderBookDebugger.snapshot(request, registry.resolve(tb))); } @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = "/order-book", method = {RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity orderBook(@Valid @RequestBody OrderBookRequest request) throws NoStreamsException + public ResponseEntity orderBook(@Valid @RequestBody OrderBookRequest request, + @RequestParam(required = false) String tb) throws NoStreamsException { OrderBookSnapshotRequest snapshotRequest = new OrderBookSnapshotRequest(); snapshotRequest.setStreams(request.streams); @@ -1788,7 +1870,7 @@ public ResponseEntity orderBook(@Valid @RequestBody OrderBookReque return ResponseEntity.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) - .body(orderBookDebugger.snapshot(snapshotRequest)); + .body(orderBookDebugger.snapshot(snapshotRequest, registry.resolve(tb))); } /** @@ -1799,12 +1881,13 @@ public ResponseEntity orderBook(@Valid @RequestBody OrderBookReque */ @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @GetMapping(value = "/availableSources", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity availableSources(@RequestParam String[] streams) { + public ResponseEntity availableSources(@RequestParam String[] streams, + @RequestParam(required = false) String tb) { String[] decodeStreams = new String[streams.length]; for (int i = 0; i < streams.length; i++) { decodeStreams[i] = URLDecoder.decode(streams[i], StandardCharsets.UTF_8); } - DXTickStream[] tickStreams = match(service, decodeStreams); + DXTickStream[] tickStreams = match(registry.resolve(tb), decodeStreams); return ResponseEntity.ok(getAvailableSources(tickStreams)); } 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 240c89da..70212ce2 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 @@ -130,7 +130,7 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor header, SubscriptionCh String subscriptionId = header.getSubscriptionId(); JsonBigIntEncoding bigIntEncoding = HeaderAccessorHelper.getJsonBigIntEncoding(header); - monitorService.subscribeTopic(sessionId, subscriptionId, topicKey, channel::sendMessage, bigIntEncoding); + monitorService.subscribeTopic(sessionId, subscriptionId, topicKey, channel::sendMessage, bigIntEncoding, null); return () -> monitorService.unsubscribe(sessionId, subscriptionId); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ViewController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ViewController.java index da715bbc..474ff740 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ViewController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/controllers/ViewController.java @@ -16,18 +16,13 @@ */ package com.epam.deltix.tbwg.webapp.controllers; -import com.epam.deltix.qsrv.hf.pub.md.ClassDescriptor; -import com.epam.deltix.qsrv.hf.pub.md.ClassSet; -import com.epam.deltix.qsrv.hf.tickdb.pub.SelectionOptions; import com.epam.deltix.tbwg.messages.ViewState; import com.epam.deltix.tbwg.webapp.model.view.SaveQueryViewInfoDef; -import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.timebase.exc.InvalidQueryException; import com.epam.deltix.tbwg.webapp.services.view.md.MutableQueryViewMd; import com.epam.deltix.tbwg.webapp.services.view.md.ViewMd; import com.epam.deltix.tbwg.webapp.services.view.ViewService; import com.epam.deltix.tbwg.webapp.services.view.md.ViewMdUtils; -import com.epam.deltix.util.parsers.CompilationException; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -40,49 +35,35 @@ @CrossOrigin public class ViewController { - private final TimebaseService timebaseService; private final ViewService viewService; - public ViewController(TimebaseService timebaseService, ViewService viewService) { - this.timebaseService = timebaseService; + public ViewController(ViewService viewService) { this.viewService = viewService; } @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = {""}, method = RequestMethod.GET) @ResponseBody - public List views() { - return viewService.list(); + public List views(@RequestParam(required = false) String tb) { + return viewService.list(tb); } @PreAuthorize("hasAnyAuthority('TB_ALLOW_READ', 'TB_ALLOW_WRITE')") @RequestMapping(value = {"/{viewId}"}, method = RequestMethod.GET) @ResponseBody - public ViewMd view(@PathVariable String viewId) { - return viewService.get(viewId); + public ViewMd view(@PathVariable String viewId, @RequestParam(required = false) String tb) { + return viewService.get(viewId, tb); } @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "", method = {RequestMethod.POST}, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ViewMd save(@RequestBody SaveQueryViewInfoDef viewMd) throws InvalidQueryException { + public ViewMd save(@RequestBody SaveQueryViewInfoDef viewMd, + @RequestParam(required = false) String tb) throws InvalidQueryException { if (viewMd.getId().contains("/") || viewMd.getId().contains("\\") || viewMd.getId().contains(" ")) { throw new RuntimeException("Invalid character in view (can't contain /, \\ or space)"); } - try { - ClassSet classSet = timebaseService.getConnection().describeQuery(viewMd.getQuery(), new SelectionOptions()); - ClassDescriptor[] descriptors = classSet.getClasses(); - for (ClassDescriptor descriptor : descriptors) { - if (descriptor.getName() == null) { - throw new NullPointerException("Query result set contains types with empty name. " + - "Use `TYPE` keyword to specify type name for result set, for example: `SELECT a, b, c TYPE MyType`"); - } - } - } catch (CompilationException e) { - throw new InvalidQueryException(viewMd.getQuery()); - } - MutableQueryViewMd info = ViewMdUtils.INSTANCE.newQueryViewInfo(); info.setId(viewMd.getId()); info.setStream(ViewService.getStreamName(viewMd.getId())); @@ -92,27 +73,29 @@ public ViewMd save(@RequestBody SaveQueryViewInfoDef viewMd) throws InvalidQuery info.setState(ViewState.CREATED); info.setInfo(null); - viewService.create(info); + viewService.create(info, tb); return info; } @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = {"/{viewId}/restart"}, method = RequestMethod.PUT) @ResponseBody - public void restart(@PathVariable String viewId, @RequestParam(required = false) Instant from) { - viewService.restart(viewId, from); + public void restart(@PathVariable String viewId, + @RequestParam(required = false) String tb, + @RequestParam(required = false) Instant from) { + viewService.restart(viewId, tb, from); } @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = {"/{viewId}/stop"}, method = RequestMethod.PUT) @ResponseBody - public void stop(@PathVariable String viewId) { - viewService.stop(viewId); + public void stop(@PathVariable String viewId, @RequestParam(required = false) String tb) { + viewService.stop(viewId, tb); } @PreAuthorize("hasAuthority('TB_ALLOW_WRITE')") @RequestMapping(value = "/{viewId}", method = {RequestMethod.DELETE}) - public void delete(@PathVariable String viewId) { - viewService.delete(viewId); + public void delete(@PathVariable String viewId, @RequestParam(required = false) String tb) { + viewService.delete(viewId, tb); } } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/interceptors/TimebaseLoginInterceptor.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/interceptors/TimebaseLoginInterceptor.java index 1037d506..c50aac55 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/interceptors/TimebaseLoginInterceptor.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/interceptors/TimebaseLoginInterceptor.java @@ -16,9 +16,13 @@ */ package com.epam.deltix.tbwg.webapp.interceptors; -import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseLoginService; +import com.epam.deltix.gflog.api.Log; +import com.epam.deltix.gflog.api.LogFactory; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; +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 org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @@ -29,19 +33,30 @@ @Component public class TimebaseLoginInterceptor implements HandlerInterceptor { - private final TimebaseLoginService timebaseLoginService; + private static final Log LOGGER = LogFactory.getLog(TimebaseLoginInterceptor.class); - public TimebaseLoginInterceptor(TimebaseLoginService timebaseLoginService) { - this.timebaseLoginService = timebaseLoginService; + private final TimebaseRegistry registry; + + public TimebaseLoginInterceptor(TimebaseRegistry registry) { + this.registry = registry; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { Principal principal = request.getUserPrincipal(); if (principal != null) { - timebaseLoginService.login(request.getUserPrincipal(), TbUserDetails.create(TBWGUtils.getIp(request))); + TbUserDetails details = TbUserDetails.create(TBWGUtils.getIp(request)); + for (TimebaseService svc : registry.getAll()) { + try { + svc.login(principal, details); + } catch (AccessDeniedException e) { + throw e; + } catch (Exception e) { + LOGGER.warn().append("Failed to login to timebase [").append(svc.getId()).append("]: ") + .append(e.getMessage()).commit(); + } + } } - return true; } @@ -49,7 +64,15 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { Principal principal = request.getUserPrincipal(); if (principal != null) { - timebaseLoginService.logout(request.getUserPrincipal(), TbUserDetails.create(TBWGUtils.getIp(request))); + TbUserDetails details = TbUserDetails.create(TBWGUtils.getIp(request)); + for (TimebaseService svc : registry.getAll()) { + try { + svc.logout(principal, details); + } catch (Exception e) { + LOGGER.warn().append("Failed to logout from timebase [").append(svc.getId()).append("]: ") + .append(e.getMessage()).commit(); + } + } } } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/StreamDef.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/StreamDef.java index 672076f3..595209e5 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/StreamDef.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/StreamDef.java @@ -40,6 +40,9 @@ public StreamDef(String key, String name, int symbols) { @JsonProperty public int symbols; + @JsonProperty + public String tbId; + @JsonProperty public ChartTypeDef[] chartType; } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/TimebaseInstanceDef.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/TimebaseInstanceDef.java new file mode 100644 index 00000000..e52a3842 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/TimebaseInstanceDef.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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.model; + +import com.epam.deltix.qsrv.hf.tickdb.client.Version; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; + +public class TimebaseInstanceDef { + + public final String id; + public final String url; + public final boolean readonly; + public final boolean connected; + public final String serverVersion; + public final String clientVersion; + public final String errorMessage; + + public TimebaseInstanceDef(TimebaseService svc) { + this.id = svc.getId(); + this.url = svc.getUrl(); + this.readonly = svc.isReadonly(); + this.connected = svc.isConnected(); + this.serverVersion = svc.getServerVersion(); + this.clientVersion = Version.getVersion(); + this.errorMessage = svc.getLastError(); + } +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/VersionDef.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/VersionDef.java index bc1b23d7..a911cb75 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/VersionDef.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/VersionDef.java @@ -21,6 +21,8 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + /** * API Version information. */ @@ -45,33 +47,11 @@ public class VersionDef { private long timestamp; /** - * TimeBase info + * TimeBase instances */ - private TimeBase timebase; + private List timebases; @JsonProperty public boolean authentication = true; - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class TimeBase { - - /** - * Client version - */ - private String clientVersion; - - /** - * Server version - */ - private String serverVersion; - - - /** - * Connection status - */ - boolean connected; - } - } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/input/ImportRequest.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/input/ImportRequest.java index d6f2ae1e..1da866f4 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/input/ImportRequest.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/input/ImportRequest.java @@ -73,4 +73,7 @@ public class ImportRequest { public LoadingOptions.WriteMode writeMode; + @JsonProperty + public String tbId; + } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/input/PlaybackRequest.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/input/PlaybackRequest.java index da581b59..7a58680f 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/input/PlaybackRequest.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/input/PlaybackRequest.java @@ -81,4 +81,16 @@ public class PlaybackRequest { @JsonProperty private boolean permanent = false; + /** + * Source TimeBase instance id. If null, the default TimeBase is used. + */ + @JsonProperty + private String sourceTb; + + /** + * Target TimeBase instance id. If null, the default TimeBase is used. + */ + @JsonProperty + private String targetTb; + } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/TreeNodeDef.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/TreeNodeDef.java index 6b78149f..f57ac8d1 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/TreeNodeDef.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/TreeNodeDef.java @@ -35,6 +35,8 @@ public class TreeNodeDef { private long childrenCount; private long totalCount; private List children = new ArrayList<>(); + private boolean available = true; + private String errorMessage; public TreeNodeDef(String id, TreeNodeType type) { this(id, id, type); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/TreeNodeType.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/TreeNodeType.java index caa2ddc9..e3bc0cc8 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/TreeNodeType.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/TreeNodeType.java @@ -17,6 +17,8 @@ package com.epam.deltix.tbwg.webapp.model.tree; public enum TreeNodeType { + /** Virtual root returned when multiple TimeBase instances are configured. */ + ROOT, DB, STREAM, VIEW, diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/events/TreeEvent.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/events/TreeEvent.java index 97f0f084..e30ac64e 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/events/TreeEvent.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/tree/events/TreeEvent.java @@ -16,17 +16,22 @@ */ package com.epam.deltix.tbwg.webapp.model.tree.events; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @Setter @Getter -@AllArgsConstructor public class TreeEvent { private TreeEventType type; private TreeEventAction action; private String id; + private String tbId; + + public TreeEvent(TreeEventType type, TreeEventAction action, String id) { + this.type = type; + this.action = action; + this.id = id; + } } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/ws/TimebaseStatusEvent.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/ws/TimebaseStatusEvent.java new file mode 100644 index 00000000..a1e509d0 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/ws/TimebaseStatusEvent.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 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.model.ws; + +public class TimebaseStatusEvent { + + public final String id; + public final boolean connected; + public final String errorMessage; + + public TimebaseStatusEvent(String id, boolean connected, String errorMessage) { + this.id = id; + this.connected = connected; + this.errorMessage = errorMessage; + } +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/ws/system/StreamStates.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/ws/system/StreamStates.java index ec56afd0..7b7db766 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/ws/system/StreamStates.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/model/ws/system/StreamStates.java @@ -30,6 +30,7 @@ public class StreamStates extends SystemMessage { protected final ConcurrentLinkedDeque renamed = new ConcurrentLinkedDeque<>(); protected AtomicLong id = new AtomicLong(); + private volatile String tbId; public void putAdded(String key) { added.add(key); @@ -95,6 +96,11 @@ public String toString() { '}'; } + @JsonProperty("tbId") + public String getTbId() { return tbId; } + + public void setTbId(String tbId) { this.tbId = tbId; } + @JsonIgnore public synchronized boolean isEmpty() { return added.isEmpty() && deleted.isEmpty() && changed.isEmpty() && renamed.isEmpty(); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/security/ResourceServerConfig.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/security/ResourceServerConfig.java index 05692cbd..5f639007 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/security/ResourceServerConfig.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/security/ResourceServerConfig.java @@ -24,6 +24,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -146,6 +148,15 @@ public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.httpFirewall(allowUrlEncodedSlashHttpFirewall()); } + @Bean + public WebServerFactoryCustomizer tomcatCustomizer() + { + return factory -> factory.addConnectorCustomizers(connector -> { + connector.setAllowBackslash(true); + connector.setEncodedSolidusHandling("DECODE"); + }); + } + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingController.java index 3757d9e4..ad46fd88 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingController.java @@ -21,9 +21,7 @@ import com.epam.deltix.tbwg.webapp.model.charting.ChartingFrameDef; import com.epam.deltix.tbwg.webapp.model.input.QueryRequest; import com.epam.deltix.tbwg.webapp.services.charting.queries.ChartingResult; -import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.timebase.exc.UnknownStreamException; -import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -41,12 +39,10 @@ @RequestMapping("/api/v0/charting") public class ChartingController { - private final TimebaseService timebaseService; private final ChartingService chartingService; @Autowired - public ChartingController(TimebaseService timebaseService, ChartingService chartingService) { - this.timebaseService = timebaseService; + public ChartingController(ChartingService chartingService) { this.chartingService = chartingService; } @@ -72,8 +68,9 @@ public List barPeriodicities() { } @RequestMapping(value = "/{streamKey}/settings/linear-chart-columns", method = RequestMethod.GET) - public String[] chartableColumns(@PathVariable String streamKey) throws UnknownStreamException { - return TBWGUtils.getLinearChartColumns(timebaseService.getStreamChecked(streamKey).getTypes()); + public String[] chartableColumns(@PathVariable String streamKey, + @RequestParam(required = false) String tb) throws UnknownStreamException { + return chartingService.linearChartColumns(streamKey, tb); } @RequestMapping(value = "/dx/{streamKey}", method = RequestMethod.GET) @@ -85,7 +82,8 @@ public ResponseEntity getData(@PathVariable String stream @RequestParam(required = false, defaultValue = "1000") long pointInterval, @RequestParam(required = false, defaultValue = "20") int levels, @RequestParam(required = false) Long correlationId, - @RequestParam(defaultValue = "L2") ModelDataSourceType source) + @RequestParam(defaultValue = "L2") ModelDataSourceType source, + @RequestParam(required = false) String tb) { if (pointInterval <= 0) { throw new RuntimeException("Illegal pointInterval value: " + pointInterval); @@ -101,7 +99,7 @@ public ResponseEntity getData(@PathVariable String stream new ChartingSettings( streamKey, null, symbols, type, new TimeInterval(startTime, endTime), - pointInterval, levels, source + pointInterval, levels, source, tb ), correlationId ); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingService.java index 3cff5b7a..b4b7b89a 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingService.java @@ -18,6 +18,7 @@ import com.epam.deltix.tbwg.webapp.model.charting.ChartingFrameDef; import com.epam.deltix.tbwg.webapp.services.charting.queries.ChartingResult; +import com.epam.deltix.tbwg.webapp.services.timebase.exc.UnknownStreamException; public interface ChartingService { @@ -28,4 +29,6 @@ public interface ChartingService { ChartingResult getDataStream(ChartingSettings settings, Long correlationId); void stopCharting(Long correlationId); + + String[] linearChartColumns(String streamKey, String tbId) throws UnknownStreamException; } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingServiceImpl.java index 49ab2fb4..787c76f3 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingServiceImpl.java @@ -25,8 +25,12 @@ import com.epam.deltix.tbwg.webapp.services.charting.provider.LinesProvider; import com.epam.deltix.tbwg.webapp.services.charting.queries.BookSymbolQueryImpl; import com.epam.deltix.tbwg.webapp.services.charting.queries.ChartingResult; +import com.epam.deltix.tbwg.webapp.services.charting.queries.LinesQueryImpl; import com.epam.deltix.tbwg.webapp.services.charting.queries.LinesQueryResult; import com.epam.deltix.tbwg.webapp.services.charting.queries.QqlQueryImpl; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; +import com.epam.deltix.tbwg.webapp.services.timebase.exc.UnknownStreamException; +import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; import com.epam.deltix.util.collections.generated.LongToLongHashMap; import com.epam.deltix.util.collections.generated.LongToObjectHashMap; import org.springframework.beans.factory.annotation.Autowired; @@ -41,13 +45,15 @@ public class ChartingServiceImpl implements ChartingService { private static final Log LOGGER = LogFactory.getLog(ChartingServiceImpl.class); private final LinesProvider provider; + private final TimebaseRegistry registry; private final LongToObjectHashMap runningTasks = new LongToObjectHashMap<>(); private final LongToLongHashMap stoppedTasks = new LongToLongHashMap(); @Autowired - public ChartingServiceImpl(LinesProvider provider) { + public ChartingServiceImpl(LinesProvider provider, TimebaseRegistry registry) { this.provider = provider; + this.registry = registry; } @Scheduled(fixedDelay = 5 * 60 * 1000) @@ -86,26 +92,31 @@ public ChartingResult getDataStream(ChartingSettings settings, Long correlationI } private ChartingResult buildChartingResult(ChartingSettings settings) { - return provider.getLines( - settings.getQql() == null ? - new BookSymbolQueryImpl( - settings.getStream(), - settings.getSymbols(), - settings.getType(), - settings.getInterval(), - settings.getPointInterval(), - settings.getLevels(), - false, - settings.getDataSource() - ) : - new QqlQueryImpl( - settings.getQql(), - settings.getType(), - settings.getInterval(), - settings.getPointInterval(), - false - ) - ); + LinesQueryImpl query = settings.getQql() == null ? + new BookSymbolQueryImpl( + settings.getStream(), + settings.getSymbols(), + settings.getType(), + settings.getInterval(), + settings.getPointInterval(), + settings.getLevels(), + false, + settings.getDataSource() + ) : + new QqlQueryImpl( + settings.getQql(), + settings.getType(), + settings.getInterval(), + settings.getPointInterval(), + false + ); + query.setService(registry.resolve(settings.getTbId())); + return provider.getLines(query); + } + + @Override + public String[] linearChartColumns(String streamKey, String tbId) throws UnknownStreamException { + return TBWGUtils.getLinearChartColumns(registry.resolve(tbId).getStreamChecked(streamKey).getTypes()); } @Override diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingSettings.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingSettings.java index d29ae600..51c45a99 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingSettings.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/ChartingSettings.java @@ -33,4 +33,11 @@ public class ChartingSettings { private long pointInterval; private int levels; private ModelDataSourceType dataSource; + private String tbId; + + public ChartingSettings(String stream, String qql, String[] symbols, ChartType type, + TimeInterval interval, long pointInterval, int levels, + ModelDataSourceType dataSource) { + this(stream, qql, symbols, type, interval, pointInterval, levels, dataSource, null); + } } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingController.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingController.java index aa09b682..acc22c3a 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingController.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingController.java @@ -50,6 +50,7 @@ public class LiveChartingController implements SubscriptionController { private static final String POINT_INTERVAL_HEADER = "pointInterval"; private static final String LEVELS_HEADER = "levels"; private static final String SOURCE_HEADER = "source"; + private static final String TB_ID_HEADER = "tbId"; private final LiveChartingService liveChartingService; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -121,14 +122,15 @@ public Subscription onSubscribe(SimpMessageHeaderAccessor headerAccessor, Subscr return subscribe( headerAccessor, channel, chartType, streamKey, instruments, - new TimeInterval(startTime, endTime), pointInterval, levels, source + new TimeInterval(startTime, endTime), pointInterval, levels, source, + headerAccessor.getFirstNativeHeader(TB_ID_HEADER) ); } private Subscription subscribe(SimpMessageHeaderAccessor headerAccessor, SubscriptionChannel channel, ChartType chartType, String stream, String[] instruments, TimeInterval timeInterval, long pointInterval, int levels, - ModelDataSourceType source) + ModelDataSourceType source, String tbId) { String sessionId = headerAccessor.getSessionId(); String subscriptionId = headerAccessor.getSubscriptionId(); @@ -147,7 +149,7 @@ private Subscription subscribe(SimpMessageHeaderAccessor headerAccessor, Subscri liveChartingService.subscribe( sessionId, subscriptionId, new ChartingSettings( - stream, null, instruments, chartType, timeInterval, pointInterval, levels, source + stream, null, instruments, chartType, timeInterval, pointInterval, levels, source, tbId ), channel ); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingServiceImpl.java index d3bd6d05..92cbdbf5 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingServiceImpl.java @@ -20,6 +20,7 @@ import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.tbwg.webapp.services.charting.provider.LinesProvider; import com.epam.deltix.tbwg.webapp.services.tasks.StompSubscriptionTaskServiceImpl; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; @@ -32,14 +33,17 @@ public class LiveChartingServiceImpl implements LiveChartingService { private final LinesProvider linesProvider; private final StompSubscriptionTaskServiceImpl subscriptionTaskService; private final SimpMessagingTemplate messagingTemplate; + private final TimebaseRegistry registry; public LiveChartingServiceImpl(LinesProvider linesProvider, StompSubscriptionTaskServiceImpl subscriptionTaskService, - SimpMessagingTemplate messagingTemplate) + SimpMessagingTemplate messagingTemplate, + TimebaseRegistry registry) { this.linesProvider = linesProvider; this.subscriptionTaskService = subscriptionTaskService; this.messagingTemplate = messagingTemplate; + this.registry = registry; } @Override @@ -50,7 +54,7 @@ public void subscribe(String sessionId, String subscriptionId, subscriptionTaskService.startTask( sessionId, subscriptionId, new LiveChartingStompSubscriptionTask( - linesProvider, chartingSettings, channel + linesProvider, chartingSettings, channel, registry ) ); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingStompSubscriptionTask.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingStompSubscriptionTask.java index c2d541ab..c9c0653f 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingStompSubscriptionTask.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/LiveChartingStompSubscriptionTask.java @@ -25,7 +25,9 @@ import com.epam.deltix.tbwg.webapp.model.charting.line.LineElementDef; import com.epam.deltix.tbwg.webapp.services.charting.provider.LinesProvider; import com.epam.deltix.tbwg.webapp.services.charting.queries.*; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.tasks.StompSubscriptionTask; +import com.epam.deltix.tbwg.webapp.services.charting.queries.LinesQueryImpl; import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; import com.epam.deltix.tbwg.webapp.websockets.subscription.SubscriptionChannel; @@ -74,30 +76,31 @@ private synchronized List> snapshot() { public LiveChartingStompSubscriptionTask(LinesProvider linesProvider, ChartingSettings chartingSettings, - SubscriptionChannel channel) + SubscriptionChannel channel, + TimebaseRegistry registry) { this.channel = channel; - this.result = linesProvider.getLines( - chartingSettings.getQql() == null ? - new BookSymbolQueryImpl( - chartingSettings.getStream(), - chartingSettings.getSymbols(), - chartingSettings.getType(), - chartingSettings.getInterval(), - chartingSettings.getPointInterval(), - chartingSettings.getLevels(), - true, - chartingSettings.getDataSource() - ) : - new QqlQueryImpl( - chartingSettings.getQql(), - chartingSettings.getType(), - chartingSettings.getInterval(), - chartingSettings.getPointInterval(), - true - ) - ); + LinesQueryImpl query = chartingSettings.getQql() == null ? + new BookSymbolQueryImpl( + chartingSettings.getStream(), + chartingSettings.getSymbols(), + chartingSettings.getType(), + chartingSettings.getInterval(), + chartingSettings.getPointInterval(), + chartingSettings.getLevels(), + true, + chartingSettings.getDataSource() + ) : + new QqlQueryImpl( + chartingSettings.getQql(), + chartingSettings.getType(), + chartingSettings.getInterval(), + chartingSettings.getPointInterval(), + true + ); + query.setService(registry.resolve(chartingSettings.getTbId())); + this.result = linesProvider.getLines(query); result.result().getLines().forEach(lineResult -> { LineElements linesElements = new LineElements(lineResult); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/datasource/MessageSourceFactory.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/datasource/MessageSourceFactory.java index d2d5e9f3..9c230e89 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/datasource/MessageSourceFactory.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/datasource/MessageSourceFactory.java @@ -17,16 +17,17 @@ package com.epam.deltix.tbwg.webapp.services.charting.datasource; import com.epam.deltix.tbwg.webapp.services.charting.TimeInterval; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import java.util.Set; public interface MessageSourceFactory { - ReactiveMessageSource buildSource(String streamName, String[] symbols, Set types, + ReactiveMessageSource buildSource(TimebaseService service, String streamName, String[] symbols, Set types, TimeInterval interval, boolean live, boolean unbound); - ReactiveMessageSource buildSource(String qql, TimeInterval interval, boolean live, boolean unbound); + ReactiveMessageSource buildSource(TimebaseService service, String qql, TimeInterval interval, boolean live, boolean unbound); - ReactiveMessageSource buildSource(String streamName, String[] symbols, String qql, TimeInterval interval, boolean live, boolean unbound); + ReactiveMessageSource buildSource(TimebaseService service, String streamName, String[] symbols, String qql, TimeInterval interval, boolean live, boolean unbound); } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/datasource/StreamMessageSourceFactory.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/datasource/StreamMessageSourceFactory.java index d0315193..06ffa588 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/datasource/StreamMessageSourceFactory.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/datasource/StreamMessageSourceFactory.java @@ -21,6 +21,7 @@ import com.epam.deltix.qsrv.hf.tickdb.pub.DXTickDB; import com.epam.deltix.qsrv.hf.tickdb.pub.DXTickStream; import com.epam.deltix.tbwg.webapp.services.charting.TimeInterval; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.timebase.messages.IdentityKey; import org.springframework.beans.factory.annotation.Autowired; @@ -38,20 +39,20 @@ public class StreamMessageSourceFactory implements MessageSourceFactory { private final long PREFETCH_INTERVAL_MS = 60 * 1000; - private final TimebaseService timebase; + private final TimebaseRegistry registry; @Value("${charting.use-interpret-codecs:false}") private boolean useInterpretCodecs; @Autowired - public StreamMessageSourceFactory(TimebaseService timebase) { - this.timebase = timebase; + public StreamMessageSourceFactory(TimebaseRegistry registry) { + this.registry = registry; } @Override - public ReactiveMessageSource buildSource(String streamName, String[] symbols, Set types, TimeInterval interval, + public ReactiveMessageSource buildSource(TimebaseService service, String streamName, String[] symbols, Set types, TimeInterval interval, boolean live, boolean unbound) { - DXTickStream stream = timebase.getStream(streamName); + DXTickStream stream = registry.resolve(service).getStream(streamName); if (stream == null) { throw new IllegalArgumentException("Can't find stream " + streamName); } @@ -76,15 +77,15 @@ public ReactiveMessageSource buildSource(String streamName, String[] symbols, Se } @Override - public ReactiveMessageSource buildSource(String qql, TimeInterval interval, boolean live, boolean unbound) { - return buildSource(null, null, qql, interval, live, unbound); + public ReactiveMessageSource buildSource(TimebaseService service, String qql, TimeInterval interval, boolean live, boolean unbound) { + return buildSource(service, null, null, qql, interval, live, unbound); } @Override - public ReactiveMessageSource buildSource(String stream, String[] symbols, String qql, TimeInterval interval, boolean live, boolean unbound) { + public ReactiveMessageSource buildSource(TimebaseService service, String stream, String[] symbols, String qql, TimeInterval interval, boolean live, boolean unbound) { LOGGER.info().append("CHART QQL QUERY: ").append(qql).commit(); - DXTickDB db = timebase.getConnection(); + DXTickDB db = registry.resolve(service).getConnection(); TimeBaseReactiveMessageSource.Builder builder = TimeBaseReactiveMessageSource.builder(db); builder.time(interval.getStartTimeMilli()); @@ -98,14 +99,14 @@ public ReactiveMessageSource buildSource(String stream, String[] symbols, String builder.interpreted(); } if (stream != null && symbols != null) { - builder.symbols(findInstruments(stream, symbols)); + builder.symbols(findInstruments(service, stream, symbols)); } return builder.build(); } - private String[] findInstruments(String streamName, String[] symbols) { - DXTickStream stream = timebase.getStream(streamName); + private String[] findInstruments(TimebaseService service, String streamName, String[] symbols) { + DXTickStream stream = registry.resolve(service).getStream(streamName); if (stream == null) { throw new IllegalArgumentException("Can't find stream " + streamName); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/provider/TransformationServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/provider/TransformationServiceImpl.java index 2e5fe33f..10c676e3 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/provider/TransformationServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/provider/TransformationServiceImpl.java @@ -24,7 +24,7 @@ import com.epam.deltix.tbwg.webapp.services.charting.datasource.MarketDataTypeLoader; import com.epam.deltix.tbwg.webapp.services.charting.queries.*; import com.epam.deltix.tbwg.webapp.services.charting.transformations.*; -import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.gflog.api.Log; import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.qsrv.hf.pub.md.ClassDescriptor; @@ -64,7 +64,7 @@ public class TransformationServiceImpl implements TransformationService { @Value("${charting.transformations.use-qll:true}") private boolean useQql; - private final TimebaseService timebaseService; + private final TimebaseRegistry registry; private final MessageSourceFactory messageSourceFactory; private interface TransformationPlanBuilder { @@ -502,16 +502,17 @@ public TransformationType(Set types, TransformationPlanBuilder planBuild } } - public TransformationServiceImpl(TimebaseService timebaseService, MessageSourceFactory messageSourceFactory) { - this.timebaseService = timebaseService; + public TransformationServiceImpl(TimebaseRegistry registry, MessageSourceFactory messageSourceFactory) { + this.registry = registry; this.messageSourceFactory = messageSourceFactory; } @Override public LinesQueryResult buildTransformationPlan(LinesQuery query) { + com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService svc = registry.resolve(query.getService()); RecordClassSet metadata = null; if (query instanceof SymbolQuery) { - metadata = timebaseService.getStreamMetadata(((SymbolQuery) query).getStream()); + metadata = svc.getStreamMetadata(((SymbolQuery) query).getStream()); } TransformationType transformationType = transformationType(query, metadata); @@ -522,13 +523,13 @@ public LinesQueryResult buildTransformationPlan(LinesQuery query) { QqlQueryPlanBuilder queryPlanBuilder = (QqlQueryPlanBuilder) planBuilder; QqlQuery qqlQuery = queryPlanBuilder.query(); source = messageSourceFactory.buildSource( - qqlQuery.getQql(), qqlQuery.getInterval(), qqlQuery.isLive(), true + svc, qqlQuery.getQql(), qqlQuery.getInterval(), qqlQuery.isLive(), true ); } else if (planBuilder instanceof LinearPlanBuilder) { if (query instanceof SymbolQuery) { SymbolQuery symbolQuery = (SymbolQuery) query; source = messageSourceFactory.buildSource( - symbolQuery.getStream(), symbolQuery.getSymbols(), + svc, symbolQuery.getStream(), symbolQuery.getSymbols(), transformationType.types, symbolQuery.getInterval(), symbolQuery.isLive(), true @@ -541,27 +542,27 @@ public LinesQueryResult buildTransformationPlan(LinesQuery query) { if (planBuilder instanceof L2PricesPlanBuilder) { L2PricesPlanBuilder l2PricesPlanBuilder = (L2PricesPlanBuilder) planBuilder; if (l2PricesPlanBuilder.buildByQuery()) { - ChartQueryGenerator qqlB = createQueryGenerator(symbolQuery, metadata); + ChartQueryGenerator qqlB = createQueryGenerator(svc, symbolQuery, metadata); source = messageSourceFactory.buildSource( - symbolQuery.getStream(), symbolQuery.getSymbols(), qqlB.generateL2PricesQuery(), + svc, symbolQuery.getStream(), symbolQuery.getSymbols(), qqlB.generateL2PricesQuery(), symbolQuery.getInterval(), symbolQuery.isLive(), false ); } } else if (planBuilder instanceof BboPlanBuilder) { BboPlanBuilder bboPlanBuilder = (BboPlanBuilder) planBuilder; if (bboPlanBuilder.buildByQuery()) { - ChartQueryGenerator qqlB = createQueryGenerator(symbolQuery, metadata); + ChartQueryGenerator qqlB = createQueryGenerator(svc, symbolQuery, metadata); source = messageSourceFactory.buildSource( - symbolQuery.getStream(), symbolQuery.getSymbols(), qqlB.generateBboQuery(), + svc, symbolQuery.getStream(), symbolQuery.getSymbols(), qqlB.generateBboQuery(), symbolQuery.getInterval(), symbolQuery.isLive(), false ); } } else if (planBuilder instanceof BarPlanBuilder) { BarPlanBuilder barPlanBuilder = (BarPlanBuilder) planBuilder; if (barPlanBuilder.buildByQuery()) { - ChartQueryGenerator qqlB = createQueryGenerator(symbolQuery, metadata); + ChartQueryGenerator qqlB = createQueryGenerator(svc, symbolQuery, metadata); source = messageSourceFactory.buildSource( - symbolQuery.getStream(), symbolQuery.getSymbols(), qqlB.generateBarQuery(), + svc, symbolQuery.getStream(), symbolQuery.getSymbols(), qqlB.generateBarQuery(), symbolQuery.getInterval(), symbolQuery.isLive(), false ); } @@ -569,7 +570,7 @@ public LinesQueryResult buildTransformationPlan(LinesQuery query) { if (source == null) { source = messageSourceFactory.buildSource( - symbolQuery.getStream(), symbolQuery.getSymbols(), + svc, symbolQuery.getStream(), symbolQuery.getSymbols(), transformationType.types, symbolQuery.getInterval(), symbolQuery.isLive(), false @@ -580,8 +581,9 @@ public LinesQueryResult buildTransformationPlan(LinesQuery query) { return planBuilder.build(source); } - private ChartQueryGenerator createQueryGenerator(BookSymbolQuery symbolQuery, RecordClassSet metadata) { - return versionHasRecord(timebaseService.getServerVersion()) ? + private ChartQueryGenerator createQueryGenerator(com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService svc, + BookSymbolQuery symbolQuery, RecordClassSet metadata) { + return versionHasRecord(svc.getServerVersion()) ? new RecordChartQueryGenerator(symbolQuery, metadata) : new UnionChartQueryGenerator(symbolQuery, metadata); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/queries/LinesQuery.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/queries/LinesQuery.java index 12d25dcd..498dd994 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/queries/LinesQuery.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/queries/LinesQuery.java @@ -18,6 +18,7 @@ import com.epam.deltix.tbwg.webapp.model.charting.ChartType; import com.epam.deltix.tbwg.webapp.services.charting.TimeInterval; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; public interface LinesQuery { @@ -28,4 +29,6 @@ public interface LinesQuery { long getPointInterval(); boolean isLive(); + + default TimebaseService getService() { return null; } } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/queries/LinesQueryImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/queries/LinesQueryImpl.java index ff5da952..d125bc4f 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/queries/LinesQueryImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/charting/queries/LinesQueryImpl.java @@ -18,6 +18,7 @@ import com.epam.deltix.tbwg.webapp.model.charting.ChartType; import com.epam.deltix.tbwg.webapp.services.charting.TimeInterval; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; public abstract class LinesQueryImpl implements LinesQuery { @@ -25,6 +26,7 @@ public abstract class LinesQueryImpl implements LinesQuery { protected final long pointInterval; protected final ChartType type; protected final boolean isLive; + private TimebaseService service; public LinesQueryImpl(ChartType type, TimeInterval interval, long pointInterval, boolean isLive) @@ -55,4 +57,13 @@ public boolean isLive() { return isLive; } + @Override + public TimebaseService getService() { + return service; + } + + public void setService(TimebaseService service) { + this.service = service; + } + } \ No newline at end of file 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 f45a0ecd..b9ac9452 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 @@ -18,6 +18,7 @@ import com.epam.deltix.gflog.api.Log; import com.epam.deltix.gflog.api.LogFactory; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.google.common.io.Resources; import com.epam.deltix.qsrv.hf.pub.md.RecordClassDescriptor; @@ -60,8 +61,8 @@ public class GenAiService { private final GenAiHelperService genAiHelperService; private final PinnedDocsBuilder pinnedBuilder; private final QueryCompiler compiler; - private final TimebaseService timebaseService; private final int maxAttempts; + private final TimebaseRegistry registry; private static final ExecutorService EXEC = Executors.newCachedThreadPool(); @@ -71,25 +72,26 @@ private record Context(AtomicBoolean stopped) {} public GenAiService(GenAiHelperService genAiHelperService, PinnedDocsBuilder pinnedBuilder, QueryCompiler compiler, - TimebaseService timebaseService, - AiApiSettings settings) { + AiApiSettings settings, + TimebaseRegistry registry) { this.genAiHelperService = genAiHelperService; this.pinnedBuilder = pinnedBuilder; this.compiler = compiler; - this.timebaseService = timebaseService; this.maxAttempts = settings.getMaxAttempts(); + this.registry = registry; } public void subscribe(String username, String userInput, - String rawStreamKeys, SubscriptionChannel channel) { + String rawStreamKeys, SubscriptionChannel channel, String tbId) { if (active.containsKey(channel)) { channel.sendError(new IllegalStateException("Already subscribed")); return; } + TimebaseService service = registry.resolve(tbId); Set streamKeys = parseStreamKeys(rawStreamKeys); Context ctx = new Context(new AtomicBoolean(false)); active.put(channel, ctx); - EXEC.submit(() -> orchestrate(username, userInput, streamKeys, channel, ctx)); + EXEC.submit(() -> orchestrate(username, userInput, streamKeys, channel, ctx, service)); } public void unsubscribe(SubscriptionChannel channel) { @@ -101,12 +103,12 @@ public void unsubscribe(SubscriptionChannel channel) { } private void orchestrate(String username, String intent, Set streamKeys, - SubscriptionChannel ch, Context ctx) { + SubscriptionChannel ch, Context ctx, TimebaseService service) { try { if (ctx.stopped().get()) return; String overview = loadOverview(); - String schema = schemaDescription(streamKeys); + String schema = schemaDescription(streamKeys, service); send(ch, QqlGenMessage.builder("PLANNING_START")); List tags; @@ -133,7 +135,7 @@ private void orchestrate(String username, String intent, Set streamKeys, String pinned = pinnedBuilder.build(username, overview, tags, intent); send(ch, QqlGenMessage.builder("DOCS_RETRIEVED").data(summaryPinned(pinned))); - iterativeGenerate(username, schema, pinned, intent, ch, ctx); + iterativeGenerate(username, schema, pinned, intent, ch, ctx, service); } catch (Throwable t) { LOG.warn("GenAI orchestration error: %s").with(t.toString()); @@ -146,7 +148,7 @@ private void orchestrate(String username, String intent, Set streamKeys, } private void iterativeGenerate(String username, String schema, String pinned, - String intent, SubscriptionChannel ch, Context ctx) { + String intent, SubscriptionChannel ch, Context ctx, TimebaseService service) { String prompt = intent; String lastQuery = ""; String lastError = null; @@ -199,7 +201,7 @@ private void iterativeGenerate(String username, String schema, String pinned, if (ctx.stopped().get()) return; lastQuery = streamed.toString().trim(); - String compileErr = compiler.compile(lastQuery); + String compileErr = compiler.compile(lastQuery, service); if (compileErr.isEmpty()) { send(ch, QqlGenMessage.builder("ATTEMPT_COMPILE_OK") @@ -243,10 +245,10 @@ private static String loadOverview() { } } - private String schemaDescription(Set streamKeys) { + private String schemaDescription(Set streamKeys, TimebaseService service) { if (streamKeys == null || streamKeys.isEmpty()) return "No streams selected."; StringBuilder b = new StringBuilder(); - TickDBClient tb = (TickDBClient) timebaseService.getConnection(); + TickDBClient tb = (TickDBClient) service.getConnection(); for (String key : streamKeys) { DXTickStream s = tb.getStream(key); if (s == null) { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/QueryCompiler.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/QueryCompiler.java index 1a55760b..6223e026 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/QueryCompiler.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/genai/QueryCompiler.java @@ -31,16 +31,11 @@ @ConditionalOnBean(AiApiSettings.class) public class QueryCompiler { private static final Log LOG = LogFactory.getLog(QueryCompiler.class); - private final TimebaseService timebaseService; - public QueryCompiler(TimebaseService timebaseService) { - this.timebaseService = timebaseService; - } - - public String compile(String query) { + public String compile(String query, TimebaseService service) { if (query == null || query.isBlank()) return "Empty query"; try { - TickDBClient tb = (TickDBClient) timebaseService.getConnection(); + TickDBClient tb = (TickDBClient) service.getConnection(); tb.compileQuery(query, new ArrayList<>()); return ""; } catch (CompilationException ce) { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/grafana/GrafanaServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/grafana/GrafanaServiceImpl.java index 928e2c73..1e0f282d 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/grafana/GrafanaServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/grafana/GrafanaServiceImpl.java @@ -23,6 +23,7 @@ import com.epam.deltix.tbwg.webapp.services.grafana.exc.NoSuchStreamException; import com.epam.deltix.tbwg.webapp.services.grafana.exc.NoSuchSymbolsException; import com.epam.deltix.tbwg.webapp.services.grafana.qql.SelectBuilder2; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.settings.GrafanaSettings; import com.epam.deltix.gflog.api.Log; @@ -85,13 +86,13 @@ public class GrafanaServiceImpl implements GrafanaService { private static final Log LOG = LogFactory.getLog(GrafanaServiceImpl.class); private static final TypeInfo INSTRUMENT_MSG = instrumentMessage(); - private final TimebaseService timebase; + private final TimebaseRegistry registry; private final FunctionsService functionsService; private final GrafanaSettings grafanaSettings; @Autowired - public GrafanaServiceImpl(TimebaseService timebase, FunctionsService functionsService, GrafanaSettings grafanaSettings) { - this.timebase = timebase; + public GrafanaServiceImpl(TimebaseRegistry registry, FunctionsService functionsService, GrafanaSettings grafanaSettings) { + this.registry = registry; this.functionsService = functionsService; this.grafanaSettings = grafanaSettings; } @@ -100,19 +101,21 @@ public GrafanaServiceImpl(TimebaseService timebase, FunctionsService functionsSe public void postConstruct() { if (Boolean.getBoolean("grafana.testStream")) { GrafanaStreamCreator creator = new GrafanaStreamCreator(new String[]{}); - creator.createAndLoad(timebase.getConnection(), "GrafanaTestStream"); + creator.createAndLoad(registry.getDefault().getConnection(), "GrafanaTestStream"); } } @Override - public DataFrame dataFrame(String refId, String rawQuery, TimeRange timeRange, boolean isVariableQuery) throws ValidationException { + public DataFrame dataFrame(String refId, String rawQuery, TimeRange timeRange, boolean isVariableQuery, + String tbId) throws ValidationException { + TimebaseService service = registry.resolve(tbId); if (isVariableQuery) { if (GrafanaUtils.isListStreams(rawQuery)) { - return GrafanaUtils.listStreams(refId, timebase.getConnection()); + return GrafanaUtils.listStreams(refId, service.getConnection()); } String streamKey = GrafanaUtils.isListSymbols(rawQuery); if (streamKey != null) { - DXTickStream stream = timebase.getStream(streamKey); + DXTickStream stream = service.getStream(streamKey); if (stream == null) { throw new NoSuchStreamException(streamKey); } @@ -120,12 +123,12 @@ public DataFrame dataFrame(String refId, String rawQuery, TimeRange timeRange, b } } SelectionOptions options = new SelectionOptions(true, false); - ClassSet schema = timebase.getConnection().describeQuery(rawQuery, options); + ClassSet schema = service.getConnection().describeQuery(rawQuery, options); if (schema == null) { throw new UnsupportedOperationException(); } MapBasedDataFrame dataFrame = new MapBasedDataFrame(refId, GrafanaUtils.convert(schema)); - try (InstrumentMessageSource source = timebase.getConnection().executeQuery(rawQuery, options, null, null, + try (InstrumentMessageSource source = service.getConnection().executeQuery(rawQuery, options, null, null, isVariableQuery ? Long.MIN_VALUE: timeRange.getFrom().toEpochMilli(), isVariableQuery ? Long.MIN_VALUE: timeRange.getTo().toEpochMilli())) { RawMessageHelper helper = new RawMessageHelper(); long endTime = timeRange.getTo().toEpochMilli(); @@ -143,9 +146,10 @@ public DataFrame dataFrame(String refId, String rawQuery, TimeRange timeRange, b } @Override - public DataFrame dataFrame(SelectQuery query, TimeRange range, int maxDataPoints, Long intervalMs) throws ValidationException, - RecordValidationException { - SelectBuilder2 selectBuilder = constructQuery(query, range); + public DataFrame dataFrame(SelectQuery query, TimeRange range, int maxDataPoints, Long intervalMs, + String tbId) throws ValidationException, RecordValidationException { + TimebaseService service = registry.resolve(tbId); + SelectBuilder2 selectBuilder = constructQuery(query, range, service); long step = calculateStep(query, range, maxDataPoints, intervalMs); if (query.getFunctions() == null || query.getFunctions().isEmpty()) { return new MutableDataFrameImpl(query.getRefId()); @@ -309,21 +313,22 @@ public List groupByViewOptions() { } @Override - public DynamicList listStreams(String template, int offset, int limit) { + public DynamicList listStreams(String template, int offset, int limit, String tbId) { + TimebaseService service = registry.resolve(tbId); DynamicList result = new DynamicList(); List list = new ObjectArrayList<>(); result.setList(list); result.setHasMore(false); List streams; if (StringUtils.isEmpty(template)) { - streams = Arrays.stream(timebase.listStreams()) + streams = Arrays.stream(service.listStreams()) .map(DXTickStream::getKey) .filter(grafanaSettings::isKeyAccepted) .sorted() .skip(offset) .collect(Collectors.toList()); } else { - streams = Arrays.stream(timebase.listStreams()) + streams = Arrays.stream(service.listStreams()) .map(DXTickStream::getKey) .filter(grafanaSettings::isKeyAccepted) .filter(key -> key.toLowerCase().contains(template.toLowerCase())) @@ -342,8 +347,10 @@ public DynamicList listStreams(String template, int offset, int limit) { } @Override - public DynamicList listSymbols(String streamKey, String template, int offset, int limit) throws NoSuchStreamException { - DXTickStream stream = timebase.getStream(streamKey); + public DynamicList listSymbols(String streamKey, String template, int offset, int limit, + String tbId) throws NoSuchStreamException { + TimebaseService service = registry.resolve(tbId); + DXTickStream stream = service.getStream(streamKey); if (stream == null) { throw new NoSuchStreamException(streamKey); } @@ -377,8 +384,9 @@ public DynamicList listSymbols(String streamKey, String template, int offset, in } @Override - public StreamSchema schema(String streamKey) throws NoSuchStreamException { - DXTickStream stream = timebase.getStream(streamKey); + public StreamSchema schema(String streamKey, String tbId) throws NoSuchStreamException { + TimebaseService service = registry.resolve(tbId); + DXTickStream stream = service.getStream(streamKey); if (stream == null) { throw new NoSuchStreamException(streamKey); } @@ -470,10 +478,10 @@ private static boolean isValid(DataField dataField) { || dataField.getType() instanceof TimeOfDayDataType; } - private SelectBuilder2 constructQuery(SelectQuery query, TimeRange range) throws NoSuchStreamException, + private SelectBuilder2 constructQuery(SelectQuery query, TimeRange range, TimebaseService service) throws NoSuchStreamException, NoSuchSymbolsException, SelectBuilder2.NoSuchFieldException, SelectBuilder2.WrongTypeException, SelectBuilder2.NoSuchTypeException { - DXTickDB db = timebase.getConnection(); + DXTickDB db = service.getConnection(); DXTickStream stream = db.getStream(query.getStream()); if (stream == null) { throw new NoSuchStreamException(query.getStream()); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/grafana/base/GrafanaService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/grafana/base/GrafanaService.java index d007f8c3..3ccd8b39 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/grafana/base/GrafanaService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/grafana/base/GrafanaService.java @@ -38,35 +38,37 @@ public interface GrafanaService { Log LOG = LogFactory.getLog(GrafanaService.class); - DataFrame dataFrame(String refId, String rawQuery, TimeRange timeRange, boolean isVariableQuery) throws ValidationException; + DataFrame dataFrame(String refId, String rawQuery, TimeRange timeRange, boolean isVariableQuery, + String tbId) throws ValidationException; - DataFrame dataFrame(SelectQuery query, TimeRange timeRange, int maxDataPoints, Long intervalMs) throws ValidationException, - RecordValidationException; + DataFrame dataFrame(SelectQuery query, TimeRange timeRange, int maxDataPoints, Long intervalMs, + String tbId) throws ValidationException, RecordValidationException; - default List dataFrames(DataQueryRequest request) throws RecordValidationException, - ValidationException { + default List dataFrames(DataQueryRequest request, String tbId) + throws RecordValidationException, ValidationException { List dataFrames = new ObjectArrayList<>(); for (SelectQuery target : request.getTargets()) { - dataFrames.add(target.isRaw() ? dataFrame(target.getRefId(), target.getRawQuery(), request.getRange(), target.isVariableQuery()) : - dataFrame(target, request.getRange(), request.getMaxDataPoints(), request.getIntervalMs())); + dataFrames.add(target.isRaw() ? + dataFrame(target.getRefId(), target.getRawQuery(), request.getRange(), target.isVariableQuery(), tbId) : + dataFrame(target, request.getRange(), request.getMaxDataPoints(), request.getIntervalMs(), tbId)); } return dataFrames; } - default List timeSeries(DataQueryRequest request) throws RecordValidationException, - ValidationException { - return dataFrames(request).stream().flatMap(df -> GrafanaUtils.convert(df).stream()).collect(Collectors.toList()); + default List timeSeries(DataQueryRequest request, String tbId) + throws RecordValidationException, ValidationException { + return dataFrames(request, tbId).stream().flatMap(df -> GrafanaUtils.convert(df).stream()).collect(Collectors.toList()); } - default List select(DataQueryRequest request) throws RecordValidationException, - ValidationException { + default List select(DataQueryRequest request, String tbId) + throws RecordValidationException, ValidationException { long start = System.currentTimeMillis(); List targets = request.getTargets(); request.setTargets(targets.stream().filter(q -> q.getView() == null || q.getView() == SelectQuery.View.DATAFRAME).collect(Collectors.toList())); List list = new ObjectArrayList<>(); - list.addAll(dataFrames(request)); + list.addAll(dataFrames(request, tbId)); request.setTargets(targets.stream().filter(q -> q.getView() == SelectQuery.View.TIMESERIES).collect(Collectors.toList())); - list.addAll(timeSeries(request)); + list.addAll(timeSeries(request, tbId)); long end = System.currentTimeMillis(); LOG.info().append("Request execution took ").append((end - start) / 1000., 3).append(" seconds.").commit(); return list; @@ -74,10 +76,11 @@ default List select(DataQueryRequest request) throws Record List groupByViewOptions(); - DynamicList listStreams(String template, int offset, int limit); + DynamicList listStreams(String template, int offset, int limit, String tbId); - DynamicList listSymbols(String streamKey, String template, int offset, int limit) throws NoSuchStreamException; + DynamicList listSymbols(String streamKey, String template, int offset, int limit, + String tbId) throws NoSuchStreamException; - StreamSchema schema(String streamKey) throws NoSuchStreamException; + StreamSchema schema(String streamKey, String tbId) throws NoSuchStreamException; } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookDebugger.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookDebugger.java index 4c16857b..44499ddd 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookDebugger.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookDebugger.java @@ -17,10 +17,11 @@ package com.epam.deltix.tbwg.webapp.services.orderbook; import com.epam.deltix.tbwg.webapp.model.orderbook.L2PackageDto; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.timebase.exc.NoStreamsException; public interface OrderBookDebugger { - L2PackageDto snapshot(OrderBookSnapshotRequest request) throws NoStreamsException; + L2PackageDto snapshot(OrderBookSnapshotRequest request, TimebaseService service) throws NoStreamsException; } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookDebuggerImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookDebuggerImpl.java index b8046150..8ed4dd8c 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookDebuggerImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookDebuggerImpl.java @@ -57,34 +57,28 @@ public class OrderBookDebuggerImpl implements OrderBookDebugger { @Value("${order-book-debugger.snapshot-lookup-ms:60000}") private long snapshotLookupMs; - private final TimebaseService timebase; - - public OrderBookDebuggerImpl(TimebaseService timebase) { - this.timebase = timebase; - } - @Override - public L2PackageDto snapshot(OrderBookSnapshotRequest request) throws NoStreamsException { + public L2PackageDto snapshot(OrderBookSnapshotRequest request, TimebaseService service) throws NoStreamsException { if (request.isReverse()) { return reverseSnapshot( - request.getStreams(), request.getSymbol(), request.getFrom(), + service, request.getStreams(), request.getSymbol(), request.getFrom(), request.getOffset(), request.getTypes(), request.getSymbols(), request.getSpace(), request.getLevel() ); } else { return snapshot( - request.getStreams(), request.getSymbol(), request.getFrom(), + service, request.getStreams(), request.getSymbol(), request.getFrom(), request.getOffset(), request.getTypes(), request.getSymbols(), request.getSpace(), request.getLevel() ); } } - private L2PackageDto snapshot(String[] streamKeys, String symbol, long startTime, long offset, + private L2PackageDto snapshot(TimebaseService service, String[] streamKeys, String symbol, long startTime, long offset, String[] types, String[] symbols, String space, DataModelType bookLevel) { boolean snapshotFound = false; boolean packageHeaderFound = false; long from = startTime == Long.MIN_VALUE ? Long.MIN_VALUE : startTime - snapshotLookupMs; - try (TickCursor cursor = select(streamKeys, symbols, from, types, space, false)) { + try (TickCursor cursor = select(service, streamKeys, symbols, from, types, space, false)) { OrderBook book = createBook(symbol, DataModelType.LEVEL_TWO); long currentOffset = -1; @@ -135,13 +129,13 @@ private L2PackageDto snapshot(String[] streamKeys, String symbol, long startTime return snapshot; } - private L2PackageDto reverseSnapshot(String[] streamKeys, String symbol, long startTime, long offset, + private L2PackageDto reverseSnapshot(TimebaseService service, String[] streamKeys, String symbol, long startTime, long offset, String[] types, String[] symbols, String space, DataModelType bookLevel) { boolean snapshotFound = false; List messages = new ArrayList<>(); long messageTimestamp = Long.MIN_VALUE; - try (TickCursor cursor = select(streamKeys, symbols, startTime, types, space, true)) { + try (TickCursor cursor = select(service, streamKeys, symbols, startTime, types, space, true)) { int currentOffset = 0; while (cursor.next()) { @@ -254,10 +248,10 @@ private L2PackageDto buildBook(long timestamp, OrderBook book) { return packageDto; } - private TickCursor select(String[] streamKeys, String[] symbols, long startTime, + private TickCursor select(TimebaseService service, String[] streamKeys, String[] symbols, long startTime, String[] types, String space, boolean reverse) throws NoStreamsException { - List streams = getStreams(streamKeys); + List streams = getStreams(service, streamKeys); SelectionOptions options = new SelectionOptions(); options.channelQOS = ChannelQualityOfService.MIN_INIT_TIME; @@ -266,18 +260,18 @@ private TickCursor select(String[] streamKeys, String[] symbols, long startTime, options.withSpace(space); options.typeLoader = new DefaultTypeLoader(); - return timebase.getConnection().select(startTime, options, types, getInstrument(streams, symbols), + return service.getConnection().select(startTime, options, types, getInstrument(streams, symbols), streams.toArray(new DXTickStream[streams.size()])); } - private List getStreams(String... streamKeys) throws NoStreamsException { + private List getStreams(TimebaseService service, String... streamKeys) throws NoStreamsException { if (streamKeys == null || streamKeys.length == 0) { throw new NoStreamsException(); } List streams = new ArrayList<>(streamKeys.length); for (String key : streamKeys) { - DXTickStream stream = timebase.getStream(key); + DXTickStream stream = service.getStream(key); if (stream != null) streams.add(stream); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookService.java index e98d5c54..29f1cfae 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookService.java @@ -25,7 +25,7 @@ public interface OrderBookService { void subscribe(String sessionId, String subscriptionId, String instrument, String[] streams, String[] hiddenExchanges, - Consumer consumer); + String tbId, Consumer consumer); void unsubscribe(String sessionId, String subscriptionId, String instrument); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookServiceImpl.java index 32c0746d..30dc8849 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/orderbook/OrderBookServiceImpl.java @@ -22,8 +22,10 @@ import com.epam.deltix.gflog.api.Log; import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.tbwg.webapp.model.orderbook.L2PackageDto; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -51,7 +53,7 @@ public class OrderBookServiceImpl implements OrderBookService { private final Table tasks = HashBasedTable.create(); - private final TimebaseService timebase; + private final TimebaseRegistry registry; private static final class Task { private final ScheduledFuture scheduledFuture; @@ -68,8 +70,9 @@ public void cancel() { } } - public OrderBookServiceImpl(TimebaseService timebase) { - this.timebase = timebase; + @Autowired + public OrderBookServiceImpl(TimebaseRegistry registry) { + this.registry = registry; } @PostConstruct @@ -99,10 +102,11 @@ public synchronized void preDestroy() { @Override public synchronized void subscribe(String sessionId, String subscriptionId, String instrument, String[] streams, String[] hiddenExchanges, - Consumer consumer) + String tbId, Consumer consumer) { + TimebaseService service = registry.resolve(tbId); OrderBookSubscription subscription = new OrderBookSubscription( - timebase, instrument, streams, hiddenExchanges, + service, instrument, streams, hiddenExchanges, useLegacyConverter, consumer ); ScheduledFuture scheduledFuture = scheduledExecutorService.scheduleAtFixedRate( 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 e0a0d225..178887b6 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 @@ -18,16 +18,17 @@ 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, JsonBigIntEncoding bigIntEncoding); + List symbols, Consumer consumer, JsonBigIntEncoding bigIntEncoding, String tbId); void subscribeTopic(String sessionId, String subscriptionId, String key, - Consumer consumer, JsonBigIntEncoding bigIntEncoding); + Consumer consumer, JsonBigIntEncoding bigIntEncoding, String tbId); 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 1f32feb4..322de45a 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 @@ -22,6 +22,7 @@ import com.google.common.collect.Table; import com.epam.deltix.qsrv.hf.pub.RawMessage; import com.epam.deltix.qsrv.util.json.JSONRawMessagePrinter; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; import com.epam.deltix.tbwg.webapp.utils.cache.CachedMessageBufferImpl; import org.springframework.stereotype.Service; @@ -42,20 +43,21 @@ public class MonitorServiceImpl implements MonitorService { private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(MAX_TASKS); private final ExecutorService executorService = Executors.newFixedThreadPool(MAX_TASKS); - private final TimebaseService timebase; + private final TimebaseRegistry registry; private final Table table = HashBasedTable.create(); - public MonitorServiceImpl(TimebaseService timebase) { - this.timebase = timebase; + public MonitorServiceImpl(TimebaseRegistry registry) { + this.registry = registry; } @Override public synchronized void subscribe(String sessionId, String subscriptionId, String key, String qql, long fromTimestamp, List types, - List symbols, Consumer consumer, JsonBigIntEncoding bigIntEncoding) + List symbols, Consumer consumer, JsonBigIntEncoding bigIntEncoding, + String tbId) { BufferedConsumer bufferedConsumer = new BufferedConsumer(WebGatewayJsonRawMessagePrinterFactory.create(bigIntEncoding)); - StreamConsumer streamConsumer = new StreamConsumer(timebase, fromTimestamp, key, qql, symbols, types, bufferedConsumer); + StreamConsumer streamConsumer = new StreamConsumer(registry.resolve(tbId), fromTimestamp, key, qql, symbols, types, bufferedConsumer); ScheduledFuture scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> { String messages = bufferedConsumer.messageBuffer.flush(); if (messages != null && !messages.isEmpty()) { @@ -69,10 +71,11 @@ public synchronized void subscribe(String sessionId, String subscriptionId, Stri @Override public synchronized void subscribeTopic(String sessionId, String subscriptionId, String key, - Consumer consumer, JsonBigIntEncoding bigIntEncoding) + Consumer consumer, JsonBigIntEncoding bigIntEncoding, + String tbId) { BufferedConsumer bufferedConsumer = new BufferedConsumer(WebGatewayJsonRawMessagePrinterFactory.create(bigIntEncoding)); - TopicConsumer topicConsumer = new TopicConsumer(timebase, key, bufferedConsumer); + TopicConsumer topicConsumer = new TopicConsumer(registry.resolve(tbId), key, bufferedConsumer); ScheduledFuture scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> { String messages = bufferedConsumer.messageBuffer.flush(); if (messages != null && !messages.isEmpty()) { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SchemaManipulationServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SchemaManipulationServiceImpl.java index 14a423b8..7de73931 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SchemaManipulationServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SchemaManipulationServiceImpl.java @@ -28,6 +28,7 @@ import com.epam.deltix.tbwg.webapp.model.input.QueryRequest; import com.epam.deltix.tbwg.webapp.model.schema.*; import com.epam.deltix.tbwg.webapp.model.schema.changes.StreamMetaDataChangeDef; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.base.SchemaManipulationService; import com.epam.deltix.tbwg.webapp.services.timebase.exc.InvalidSchemaChangeException; import com.epam.deltix.tbwg.webapp.services.timebase.exc.TimebaseExceptions; @@ -55,12 +56,12 @@ public class SchemaManipulationServiceImpl implements SchemaManipulationService private static final Log LOG = LogFactory.getLog(SchemaManipulationService.class); - private final TimebaseService service; + private final TimebaseRegistry registry; private final ThreadLocal matcher = ThreadLocal.withInitial(() -> PATTERN.matcher("")); @Autowired - public SchemaManipulationServiceImpl(TimebaseService service) { - this.service = service; + public SchemaManipulationServiceImpl(TimebaseRegistry registry) { + this.registry = registry; } @Override @@ -70,7 +71,7 @@ public DataTypeDef[] allTypes() { List types = new ArrayList<>(); // is nanoseconds supported on server? - boolean nsSupported = VersionUtils.versionHasNsEncoding(service.getServerVersion()); + boolean nsSupported = VersionUtils.versionHasNsEncoding(registry.getDefault().getServerVersion()); for (DataType type : allTypes) { if (type.getCode() == DataType.T_DATE_TIME_TYPE) { @@ -85,13 +86,16 @@ public DataTypeDef[] allTypes() { @Override public SchemaDef describe(QueryRequest select, boolean tree) { + return describe(registry.getDefault(), select, tree); + } + + @Override + public SchemaDef describe(TimebaseService svc, QueryRequest select, boolean tree) { SelectionOptions options = select.getSelectionOptions(); LOG.info().append("DESCRIBE QUERY (").append(select.query).append(")").commit(); - DXTickDB connection = service.getConnection(); - - ClassSet metaData = connection.describeQuery(select.query, options); + ClassSet metaData = svc.getConnection().describeQuery(select.query, options); ClassDescriptor[] top = metaData.getContentClasses(); ClassDescriptor[] classes = metaData.getClasses(); @@ -115,12 +119,22 @@ public SchemaDef describe(QueryRequest select, boolean tree) { @Override public DescribeResponse describeStream(String key) throws UnknownStreamException { - return DescribeResponse.create(getStream(key)); + return describeStream(registry.getDefault(), key); + } + + @Override + public DescribeResponse describeStream(TimebaseService svc, String key) throws UnknownStreamException { + return DescribeResponse.create(getStream(svc, key)); } @Override public SchemaDef schema(String key, boolean tree) throws UnknownStreamException { - DXTickStream stream = getStream(key); + return schema(registry.getDefault(), key, tree); + } + + @Override + public SchemaDef schema(TimebaseService svc, String key, boolean tree) throws UnknownStreamException { + DXTickStream stream = getStream(svc, key); RecordClassSet metaData = stream.getStreamOptions().getMetaData(); @@ -146,7 +160,12 @@ public SchemaDef schema(String key, boolean tree) throws UnknownStreamException @Override public SchemaDef createStream(@NotNull String key, @NotNull SchemaDef schemaDef) throws WriteOperationsException { - if (service.isReadonly()) { + return createStream(registry.getDefault(), key, schemaDef); + } + + @Override + public SchemaDef createStream(TimebaseService svc, @NotNull String key, @NotNull SchemaDef schemaDef) throws WriteOperationsException { + if (svc.isReadonly()) { throw TimebaseExceptions.createStreamForbidden(); } @@ -156,14 +175,20 @@ public SchemaDef createStream(@NotNull String key, @NotNull SchemaDef schemaDef) LOG.info().append("CREATE STREAM (").append(key).append(")").commit(); - DXTickStream stream = service.getConnection().createStream(key, options); + DXTickStream stream = svc.getConnection().createStream(key, options); return SchemaBuilder.toSchemaDef(stream.getStreamOptions().getMetaData(), false); } @Override public StreamMetaDataChangeDef schemaChanges(@NotNull String key, @NotNull SchemaChangesRequest schemaChangesRequest) throws UnknownStreamException { - DXTickStream stream = getStream(key); + return schemaChanges(registry.getDefault(), key, schemaChangesRequest); + } + + @Override + public StreamMetaDataChangeDef schemaChanges(TimebaseService svc, @NotNull String key, @NotNull SchemaChangesRequest schemaChangesRequest) + throws UnknownStreamException { + DXTickStream stream = getStream(svc, key); RecordClassSet source = stream.getStreamOptions().getMetaData(); RecordClassSet target = SchemaBuilder.toClassSet(schemaChangesRequest.getSchema()); SchemaAnalyzer schemaAnalyzer = new SchemaAnalyzer(toSchemaMapping(schemaChangesRequest.getSchemaMapping(), @@ -179,11 +204,17 @@ public StreamMetaDataChangeDef schemaChanges(@NotNull String key, @NotNull Schem @Override public SchemaDef changeSchema(@NotNull String key, @NotNull ChangeSchemaRequest changeSchemaRequest) throws UnknownStreamException, WriteOperationsException, InvalidSchemaChangeException { - if (service.isReadonly()) { + return changeSchema(registry.getDefault(), key, changeSchemaRequest); + } + + @Override + public SchemaDef changeSchema(TimebaseService svc, @NotNull String key, @NotNull ChangeSchemaRequest changeSchemaRequest) + throws UnknownStreamException, WriteOperationsException, InvalidSchemaChangeException { + if (svc.isReadonly()) { throw TimebaseExceptions.schemaChangeForbidden(); } - DXTickStream stream = getStream(key); + DXTickStream stream = getStream(svc, key); RecordClassSet source = stream.getStreamOptions().getMetaData(); RecordClassSet target = SchemaBuilder.toClassSet(changeSchemaRequest.getSchema()); SchemaAnalyzer schemaAnalyzer = new SchemaAnalyzer(toSchemaMapping(changeSchemaRequest.getSchemaMapping(), @@ -227,7 +258,11 @@ public SchemaDef getSchema(String key) { } private DXTickStream getStream(String key) throws UnknownStreamException { - DXTickStream stream = service.getStream(key); + return getStream(registry.getDefault(), key); + } + + private DXTickStream getStream(TimebaseService svc, String key) throws UnknownStreamException { + DXTickStream stream = svc.getStream(key); if (stream == null) throw new UnknownStreamException(key); 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 67f7070c..687578e4 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 @@ -19,6 +19,7 @@ import com.epam.deltix.gflog.api.Log; import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.qsrv.hf.pub.ChannelQualityOfService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; 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; @@ -44,11 +45,11 @@ public class SelectServiceImpl implements SelectService { private static final Log LOG = LogFactory.getLog(SelectServiceImpl.class); - private final TimebaseService timebase; + private final TimebaseRegistry registry; @Autowired - public SelectServiceImpl(TimebaseService timebase) { - this.timebase = timebase; + public SelectServiceImpl(TimebaseRegistry registry) { + this.registry = registry; } @Override @@ -56,13 +57,20 @@ public MessageSource2ResponseStream select(long startTime, long endTime, long of String[] types, String[] symbols, String[] keys, String space, int maxRecords, JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { + return select(registry.getDefault(),startTime, endTime, offset, rows, reverse, types, symbols, keys, space, maxRecords, bigIntEncoding); + } + + @Override + public MessageSource2ResponseStream select(TimebaseService svc, long startTime, long endTime, long offset, int rows, boolean reverse, + String[] types, String[] symbols, String[] keys, String space, int maxRecords, + JsonBigIntEncoding bigIntEncoding) + throws NoStreamsException { - List streams = getStreams(keys); + List streams = getStreams(svc, keys); HashSet instruments = getInstruments(streams, symbols); SelectionOptions options = getSelectionOptions(reverse); final long startIndex = offset < 0 ? 0 : offset; final long endIndex = startIndex + rows - 1; // inclusive - //DXTickStream[] tickStreams = streams.toArray(new DXTickStream[streams.size()]); TickCursor source; @@ -70,7 +78,7 @@ public MessageSource2ResponseStream select(long startTime, long endTime, long of options.withSpace(space); source = streams.get(0).select(startTime, options, types, collect(instruments)); } else { - source = timebase.getConnection().select(startTime, options, types, collect(instruments), + source = svc.getConnection().select(startTime, options, types, collect(instruments), streams.toArray(new DXTickStream[streams.size()])); } @@ -88,19 +96,28 @@ public MessageSource2ResponseStream select(long startTime, long endTime, long of @Override public MessageSource2ResponseStream select(SelectRequest selectRequest, int maxRecords, JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { - List streams = getStreams(selectRequest.streams); + return select(registry.getDefault(),selectRequest, maxRecords, bigIntEncoding); + } + + @Override + public MessageSource2ResponseStream select(TimebaseService svc, SelectRequest selectRequest, int maxRecords, JsonBigIntEncoding bigIntEncoding) throws NoStreamsException { + List streams = getStreams(svc, selectRequest.streams); long startTime = selectRequest.getStartTime(getEndTime(streams)); - return select(startTime, selectRequest.getEndTime(), selectRequest.offset, selectRequest.rows, selectRequest.reverse, + return select(svc, startTime, selectRequest.getEndTime(), selectRequest.offset, selectRequest.rows, selectRequest.reverse, selectRequest.types, selectRequest.symbols, selectRequest.streams, selectRequest.space, maxRecords, bigIntEncoding); } private List getStreams(String ... streamKeys) throws NoStreamsException { + return getStreams(registry.getDefault(), streamKeys); + } + + private List getStreams(TimebaseService svc, String ... streamKeys) throws NoStreamsException { if (streamKeys == null || streamKeys.length == 0) { throw new NoStreamsException(); } List streams = new ArrayList<>(streamKeys.length); for (String key : streamKeys) { - DXTickStream stream = timebase.getStream(key); + DXTickStream stream = svc.getStream(key); if (stream != null) streams.add(stream); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SystemMessagesService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SystemMessagesService.java index f2dd568f..6cde1e3e 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SystemMessagesService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/SystemMessagesService.java @@ -30,6 +30,7 @@ import jakarta.annotation.PostConstruct; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @Service @@ -42,6 +43,7 @@ public class SystemMessagesService { private final StreamsStateListener masterListener; private final Map userToListener = new HashMap<>(); + private final Map tbListeners = new ConcurrentHashMap<>(); interface SubscribeUserListener { void subscribed(String user, SystemMessagesNotifier notifier); @@ -66,11 +68,21 @@ public void logStart() { @Scheduled(fixedDelay = 1000) public void broadcastStreamsState() { masterListener.broadcastEvents(); + tbListeners.forEach((tbId, listener) -> listener.broadcastEvents()); synchronized (userToListener) { userToListener.forEach((k, v) -> v.broadcastEvents()); } } + public StreamsStateListener getStateListenerForTb(String tbId) { + if (tbId == null) return masterListener; + return tbListeners.computeIfAbsent(tbId, id -> { + StreamsStateListener l = new StreamsStateListener(template); + l.setTbId(id); + return l; + }); + } + public StreamsStateListener getStateListener() { return masterListener; } @@ -99,6 +111,7 @@ public static class StreamsStateListener implements DBStateListener, SystemMessa private final SimpMessagingTemplate template; private final String endpoint; + private String tbId; private final StreamStates streamStates = new StreamStates(); @@ -114,10 +127,15 @@ public StreamsStateListener(SimpMessagingTemplate template, String user) { this.endpoint = WebSocketConfig.STREAMS_TOPIC + "/" + user; } + public void setTbId(String tbId) { + this.tbId = tbId; + } + public void broadcastEvents() { try { synchronized (streamStates) { if (!streamStates.isEmpty()) { + streamStates.setTbId(tbId); template.convertAndSend(endpoint, streamStates); if (LOG.isTraceEnabled()) { LOG.trace().append("Send message to topic ") diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseRegistry.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseRegistry.java new file mode 100644 index 00000000..a6e6dc53 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseRegistry.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 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.timebase; + +import java.util.List; + +/** + * Registry of all configured TimeBase instances. + * Provides access to individual instances by id and to the default (first) instance. + */ +public interface TimebaseRegistry { + + /** + * Returns all configured TimeBase service instances in configuration order. + */ + List getAll(); + + /** + * Returns the TimeBase service by its configured id. + * + * @throws IllegalArgumentException if no instance with the given id exists + */ + TimebaseService getById(String id); + + /** + * Returns the first (default) TimeBase service instance. + */ + TimebaseService getDefault(); + + /** + * Resolves a TimeBase instance by id. Returns the default instance when {@code id} is null or empty. + * + * @throws IllegalArgumentException if {@code id} is non-empty but unknown + */ + default TimebaseService resolve(String id) { + return id != null && !id.isEmpty() ? getById(id) : getDefault(); + } + + /** + * Returns {@code service} if non-null, otherwise the default instance. + */ + default TimebaseService resolve(TimebaseService service) { + return service != null ? service : getDefault(); + } +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseRegistryImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseRegistryImpl.java new file mode 100644 index 00000000..db3264e3 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseRegistryImpl.java @@ -0,0 +1,116 @@ +/* + * Copyright 2024 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.timebase; + +import com.epam.deltix.gflog.api.Log; +import com.epam.deltix.gflog.api.LogFactory; +import com.epam.deltix.tbwg.webapp.services.timebase.connections.TbUserConnectionsService; +import com.epam.deltix.tbwg.webapp.settings.TimebaseSettings; +import com.epam.deltix.tbwg.webapp.settings.TimebasesListSettings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PreDestroy; +import java.util.*; + +@Service +@Profile("default") +public class TimebaseRegistryImpl implements TimebaseRegistry { + + public static final String DEFAULT_ID = "TimeBase"; + + private static final Log LOGGER = LogFactory.getLog(TimebaseRegistryImpl.class); + + private final List instances; + private final Map instancesById; + private final boolean ownInstances; + + @Autowired + public TimebaseRegistryImpl( + TimebaseService legacyService, + TimebaseSettings legacySettings, + TimebasesListSettings listSettings, + SystemMessagesService systemMessagesService, + TbUserConnectionsService userConnectionsService) { + + List configs = listSettings.getTimebases(); + + if (configs.isEmpty()) { + if (legacySettings.getId() == null || legacySettings.getId().isEmpty()) { + legacySettings.setId(DEFAULT_ID); + } + instances = Collections.singletonList(legacyService); + ownInstances = false; + LOGGER.info("TimebaseRegistry: single-instance mode, id='%s' -> %s.") + .with(legacySettings.getId()).with(legacySettings.getUrl()); + } else { + List list = new ArrayList<>(configs.size()); + for (int i = 0; i < configs.size(); i++) { + TimebaseSettings settings = configs.get(i); + if (settings.getId() == null || settings.getId().isEmpty()) { + settings.setId(settings.getUrl()); + } + list.add(new TimebaseServiceImpl(settings, systemMessagesService, userConnectionsService)); + LOGGER.info("TimebaseRegistry: registered instance '%s' -> %s.") + .with(settings.getId()).with(settings.getUrl()); + } + instances = Collections.unmodifiableList(list); + ownInstances = true; + } + + LinkedHashMap byId = new LinkedHashMap<>(); + for (TimebaseService svc : instances) { + String id = svc.getId(); + if (id != null && !id.isEmpty()) { + byId.put(id, svc); + } + } + instancesById = Collections.unmodifiableMap(byId); + } + + @PreDestroy + public void dispose() { + if (ownInstances) { + for (TimebaseService svc : instances) { + if (svc instanceof TimebaseServiceImpl) { + ((TimebaseServiceImpl) svc).dispose(); + } + } + } + // In single-instance mode the Spring-managed TimebaseServiceImpl handles its own @PreDestroy. + } + + @Override + public List getAll() { + return instances; + } + + @Override + public TimebaseService getById(String id) { + TimebaseService svc = instancesById.get(id); + if (svc == null) { + throw new IllegalArgumentException("Unknown TimeBase id: " + id); + } + return svc; + } + + @Override + public TimebaseService getDefault() { + return instances.get(0); + } +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseService.java index 3501aac7..db5cb8b1 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseService.java @@ -58,6 +58,8 @@ default RecordClassSet getStreamMetadata(String streamName) { boolean isConnected(); + String getLastError(); + boolean isReadonly(); DXTickStream getCurrenciesStream(); @@ -70,5 +72,7 @@ default RecordClassSet getStreamMetadata(String streamName) { String getId(); + String getUrl(); + TopicDB getTopicDB(); } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseServiceImpl.java index 90ba8603..5cc261e9 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseServiceImpl.java @@ -73,6 +73,7 @@ public class TimebaseServiceImpl implements TimebaseService { private DXTickDB db = null; private String dbUrl = null; private String serverVersion = null; + private volatile String lastError = null; private final TbUserConnectionsService userConnectionsService; @@ -94,7 +95,8 @@ public String getServerVersion() { if (serverVersion == null) { try { getConnection(); - } catch (Exception ignored) { + } catch (Exception ex) { + lastError = describeError(ex); } } return serverVersion; @@ -103,15 +105,38 @@ public String getServerVersion() { @Override public boolean isConnected() { try { - return ((TickDBClient)getConnection()).isConnected(); + boolean connected = ((TickDBClient)getConnection()).isConnected(); + if (connected) { + lastError = null; + } + return connected; } catch (Exception ex) { + lastError = describeError(ex); return false; } } + @Override + public String getLastError() { + return lastError; + } + + private static String describeError(Throwable e) { + return e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName(); + } + @Override public String getId() { - return db.getId(); + String configuredId = timebaseSettings.getId(); + if (configuredId != null && !configuredId.isEmpty()) { + return configuredId; + } + return db != null ? db.getId() : null; + } + + @Override + public String getUrl() { + return timebaseSettings.getUrl(); } @Override @@ -238,7 +263,7 @@ public synchronized DXTickDB getOrCreate(String url, String userName, String pas } if (db instanceof DBStateNotifier) { - ((DBStateNotifier) db).addStateListener(systemMessagesService.getStateListener()); + ((DBStateNotifier) db).addStateListener(systemMessagesService.getStateListenerForTb(getId())); } else { LOGGER.error().append("Cannot add ") .append(DBStateListener.class.getSimpleName()) diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseStatusMonitor.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseStatusMonitor.java new file mode 100644 index 00000000..9ce99a00 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TimebaseStatusMonitor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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.timebase; + +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.model.ws.TimebaseStatusEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Service +public class TimebaseStatusMonitor { + + private static final Log LOGGER = LogFactory.getLog(TimebaseStatusMonitor.class); + + private final TimebaseRegistry registry; + private final SimpMessagingTemplate template; + + private final ConcurrentMap lastKnownConnected = new ConcurrentHashMap<>(); + + @Autowired + public TimebaseStatusMonitor(TimebaseRegistry registry, SimpMessagingTemplate template) { + this.registry = registry; + this.template = template; + } + + @Scheduled(fixedDelayString = "${timebase.status.poll-period-ms:10000}") + public void pollStatus() { + for (TimebaseService tb : registry.getAll()) { + String tbId = tb.getId(); + boolean connected = tb.isConnected(); + + Boolean previous = lastKnownConnected.put(tbId, connected); + if (previous == null) { + // First observation of this instance: just record the baseline, don't notify. + continue; + } + + if (previous != connected) { + LOGGER.info().append("Timebase [").append(tbId).append("] status changed: connected=") + .append(connected).commit(); + template.convertAndSend( + WebSocketConfig.TIMEBASE_STATUS_TOPIC, + new TimebaseStatusEvent(tbId, connected, connected ? null : tb.getLastError()) + ); + } + } + } +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TreeEventsService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TreeEventsService.java index 64568ebe..5d7f50d9 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TreeEventsService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/TreeEventsService.java @@ -45,25 +45,36 @@ public class TreeEventsService implements SystemMessagesService.SubscribeUserLis private final ViewService viewService; private final TopicService topicService; private final PlaybackService playbackService; + private final TimebaseRegistry registry; private final TreeEventsListener masterEventsListener; private final Map userToListener = new HashMap<>(); + private final Map tbEventsListeners = new HashMap<>(); public TreeEventsService(SimpMessagingTemplate template, SystemMessagesService systemMessagesService, ViewService viewService, TopicService topicService, - PlaybackService playbackService) { + PlaybackService playbackService, + TimebaseRegistry registry) { this.template = template; this.systemMessagesService = systemMessagesService; this.viewService = viewService; this.topicService = topicService; this.playbackService = playbackService; + this.registry = registry; + // Per-TB listeners handle stream DB state events with tbId context. + for (TimebaseService tb : registry.getAll()) { + TreeEventsListener tbListener = new TreeEventsListener(template, viewService, tb.getId()); + systemMessagesService.getStateListenerForTb(tb.getId()).subscribe(tbListener); + tbEventsListeners.put(tb.getId() != null ? tb.getId() : "", tbListener); + } + + // masterEventsListener handles views, topics and playback (no DB-state events). this.masterEventsListener = new TreeEventsListener(template, viewService); systemMessagesService.setSubscribeUserListener(this); - systemMessagesService.masterNotifier().subscribe(masterEventsListener); this.viewService.subscribe(masterEventsListener); this.topicService.subscribe(masterEventsListener); this.playbackService.subscribe(masterEventsListener); @@ -72,6 +83,7 @@ public TreeEventsService(SimpMessagingTemplate template, @Scheduled(fixedDelay = 1000) public void broadcastStreamsState() { masterEventsListener.broadcastEvents(); + tbEventsListeners.forEach((tbId, listener) -> listener.broadcastEvents()); synchronized (userToListener) { userToListener.forEach((k, v) -> v.broadcastEvents()); } @@ -79,10 +91,11 @@ public void broadcastStreamsState() { @PreDestroy public void preDestroy() { - systemMessagesService.masterNotifier().unsubscribe(masterEventsListener); viewService.unsubscribe(masterEventsListener); topicService.unsubscribe(masterEventsListener); playbackService.unsubscribeListener(masterEventsListener); + tbEventsListeners.forEach((tbId, listener) -> + systemMessagesService.getStateListenerForTb(tbId.isEmpty() ? null : tbId).unsubscribe(listener)); } @Override @@ -103,6 +116,7 @@ public static class TreeEventsListener implements DBStateListener, ViewListener, private final SimpMessagingTemplate template; private final String endpoint; + private final String tbId; private final ViewService viewService; @@ -112,12 +126,22 @@ public TreeEventsListener(SimpMessagingTemplate template, ViewService viewServic this.template = template; this.endpoint = WebSocketConfig.STRUCTURE_EVENTS_TOPIC; this.viewService = viewService; + this.tbId = null; } public TreeEventsListener(SimpMessagingTemplate template, String user, ViewService viewService) { this.template = template; this.endpoint = WebSocketConfig.STRUCTURE_EVENTS_TOPIC + "/" + user; this.viewService = viewService; + this.tbId = null; + } + + /** Per-TB master listener — same broadcast endpoint as master but carries tbId on events. */ + public TreeEventsListener(SimpMessagingTemplate template, ViewService viewService, String tbId) { + this.template = template; + this.endpoint = WebSocketConfig.STRUCTURE_EVENTS_TOPIC; + this.viewService = viewService; + this.tbId = tbId; } public void broadcastEvents() { @@ -235,6 +259,7 @@ public void topicRename(String topicKey, String newKey) { } private void addEvent(TreeEvent event) { + event.setTbId(tbId); synchronized (events) { events.add(event); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SchemaManipulationService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SchemaManipulationService.java index 6714c105..d803129d 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SchemaManipulationService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/base/SchemaManipulationService.java @@ -25,76 +25,35 @@ import com.epam.deltix.tbwg.webapp.services.timebase.exc.UnknownStreamException; import com.epam.deltix.tbwg.webapp.services.timebase.exc.WriteOperationsException; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; + import javax.annotation.Nonnull; public interface SchemaManipulationService { - /** - * List all data types - * @return all types array - */ DataTypeDef[] allTypes(); - /** - * Describe query - * @param queryRequest query request - * @return schema, that describes request - */ SchemaDef describe(QueryRequest queryRequest, boolean tree); + SchemaDef describe(TimebaseService svc, QueryRequest queryRequest, boolean tree); - /** - * Stream DDL description. - * @param key stream key - * @return object that contains DDL description. - * @throws UnknownStreamException if stream does not exist - */ DescribeResponse describeStream(String key) throws UnknownStreamException; + DescribeResponse describeStream(TimebaseService svc, String key) throws UnknownStreamException; - /** - * Get stream schema - * @param key stream key - * @return stream schema - * @throws UnknownStreamException if stream does not exist - */ SchemaDef schema(String key, boolean tree) throws UnknownStreamException; + SchemaDef schema(TimebaseService svc, String key, boolean tree) throws UnknownStreamException; - /** - * Create stream with provided key, schema and distribution factor. - * @param key stream key - * @param schemaDef stream schema - * @return new stream schema - * @throws WriteOperationsException if user isn't allowed to create streams - */ - SchemaDef createStream(@Nonnull String key, @Nonnull SchemaDef schemaDef) - throws WriteOperationsException; + SchemaDef createStream(@Nonnull String key, @Nonnull SchemaDef schemaDef) throws WriteOperationsException; + SchemaDef createStream(TimebaseService svc, @Nonnull String key, @Nonnull SchemaDef schemaDef) throws WriteOperationsException; - /** - * List changes between current stream schema and provided by user. - * @param key stream key - * @param schemaChangesRequest new schema and schema mapping - * @return changes - * @throws UnknownStreamException if stream does not exist - */ StreamMetaDataChangeDef schemaChanges(@Nonnull String key, @Nonnull SchemaChangesRequest schemaChangesRequest) throws UnknownStreamException; + StreamMetaDataChangeDef schemaChanges(TimebaseService svc, @Nonnull String key, @Nonnull SchemaChangesRequest schemaChangesRequest) + throws UnknownStreamException; - /** - * Execute schema change. - * @param key stream key - * @param changeSchemaRequest new stream schema, schema mapping and default values - * @return new stream schema - * @throws UnknownStreamException if stream does not exist - * @throws WriteOperationsException if timebase is readonly - * @throws InvalidSchemaChangeException if some of default values are missing - */ SchemaDef changeSchema(@Nonnull String key, @Nonnull ChangeSchemaRequest changeSchemaRequest) throws UnknownStreamException, WriteOperationsException, InvalidSchemaChangeException; + SchemaDef changeSchema(TimebaseService svc, @Nonnull String key, @Nonnull ChangeSchemaRequest changeSchemaRequest) + throws UnknownStreamException, WriteOperationsException, InvalidSchemaChangeException; - /** - * Returns schema for the specified message type. - * - * @param key message type key - * @return schema for message type - */ SchemaDef getSchema(String key); } \ No newline at end of file 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 1970cab7..8cb03824 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 @@ -17,6 +17,7 @@ package com.epam.deltix.tbwg.webapp.services.timebase.base; import com.epam.deltix.tbwg.webapp.model.input.SelectRequest; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; 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; @@ -28,6 +29,13 @@ MessageSource2ResponseStream select(long startTime, long endTime, long offset, i JsonBigIntEncoding bigIntEncoding) throws NoStreamsException; + MessageSource2ResponseStream select(TimebaseService svc, long startTime, long endTime, long offset, int rows, boolean reverse, + String[] types, String[] symbols, String[] keys, String space, int maxRecords, + JsonBigIntEncoding bigIntEncoding) + throws NoStreamsException; + MessageSource2ResponseStream select(SelectRequest selectRequest, int maxRecords, JsonBigIntEncoding bigIntEncoding) throws NoStreamsException; + MessageSource2ResponseStream select(TimebaseService svc, 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/services/timebase/csvimport/CsvImportData.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportData.java index ce24e493..56e1a25b 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportData.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportData.java @@ -28,6 +28,7 @@ public class CsvImportData { private final String id; private final String streamKey; + private final String tbId; private Map csvPreviewData = new HashMap<>(); private long processId = -1; @@ -36,9 +37,10 @@ public class CsvImportData { private ImportStatus importStatus; private volatile boolean isActive = true; - public CsvImportData(String id, String streamKey) { + public CsvImportData(String id, String streamKey, String tbId) { this.id = id; this.streamKey = streamKey; + this.tbId = tbId; } public boolean clearProcessResources() { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportService.java index b5e11db7..e31ed12c 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportService.java @@ -53,7 +53,7 @@ public interface CsvImportService { CsvImportSettings generateDefaultSettings(String id, String streamKey); - String initImport(String streamKey); + String initImport(String streamKey, String tbId); void addPreview(String id, MultipartFile files, boolean fullFile); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportServiceImpl.java index cab29d4b..2f2f6c6f 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/csvimport/CsvImportServiceImpl.java @@ -23,7 +23,7 @@ import com.epam.deltix.qsrv.hf.tickdb.pub.DXTickStream; import com.epam.deltix.qsrv.hf.tickdb.ui.tbshell.TickDBShell; import com.epam.deltix.tbwg.webapp.model.input.*; -import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.export.imp.*; import com.epam.deltix.tbwg.webapp.utils.CsvImportUtil; import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; @@ -55,22 +55,22 @@ public class CsvImportServiceImpl implements CsvImportService { private static final Log LOGGER = LogFactory.getLog(CsvImportServiceImpl.class); public static final int IMPORT_FINISH_DELAY = 60_000; - private final TimebaseService timebaseService; + private final TimebaseRegistry registry; private final UploadFileService uploadFileService; private final ExecutorService executorService = Executors.newCachedThreadPool(); private final Map importDataMap = new ConcurrentHashMap<>(); public static int PREVIEW_SIZE; - public CsvImportServiceImpl(TimebaseService timebaseService, UploadFileService uploadFileService) { - this.timebaseService = timebaseService; + public CsvImportServiceImpl(TimebaseRegistry registry, UploadFileService uploadFileService) { + this.registry = registry; this.uploadFileService = uploadFileService; } @Override - public String initImport(String streamKey) { + public String initImport(String streamKey, String tbId) { String id = UUID.randomUUID().toString(); - importDataMap.put(id, new CsvImportData(id, streamKey)); + importDataMap.put(id, new CsvImportData(id, streamKey, tbId)); return id; } @@ -125,6 +125,7 @@ private void determineTimestampParamForPreview(Preview preview, byte[] data) { @Override public CsvImportSettings generateDefaultSettings(String id, String streamKey) { Map previewMap = getPreviewMap(id); + String tbId = getImportById(id).getTbId(); CsvImportSettings settings = new CsvImportSettings(); CsvImportGeneralSettings generalSettings = new CsvImportGeneralSettings(); @@ -137,7 +138,7 @@ public CsvImportSettings generateDefaultSettings(String id, String streamKey) { } generalSettings.setCharset(CsvImportUtil.determineCharset(previewMap)); - Map typeMappings = getTypeMappings(streamKey); + Map typeMappings = getTypeMappings(streamKey, tbId); generalSettings.setTypeToKeywordMapping(typeMappings); settings.setMappings(getMappings(id, generalSettings)); @@ -149,7 +150,7 @@ public CsvImportSettings generateDefaultSettings(String id, String streamKey) { generalSettings.setDataTimeFormat(DEFAULT_DATETIME_FORMAT); } - generalSettings.setSymbols(getStreamSymbols(streamKey)); + generalSettings.setSymbols(getStreamSymbols(streamKey, tbId)); if (CsvImportUtil.isAllFullFile(previewMap)) { generalSettings.setStartTime(Instant.ofEpochMilli(findStartTime(previewMap))); generalSettings.setEndTime(Instant.ofEpochMilli(findEndTime(previewMap))); @@ -203,23 +204,24 @@ public SchemaDef generateSchema(String id, boolean enumCheck, int enumValuesCoun } // generate schema + String tbId = getImportById(id).getTbId(); CsvSchemaParser csvSchemaParser = new CsvSchemaParser(generalSettings, enumCheck, enumValuesCount, - enumRepeatRate, staticCheck, !VersionUtils.versionHasNsEncoding(timebaseService.getServerVersion())); + enumRepeatRate, staticCheck, !VersionUtils.versionHasNsEncoding(registry.resolve(tbId).getServerVersion())); for (File file : importProcess.filesList()) { csvSchemaParser.processFile(file); } return csvSchemaParser.getSchema(); } - private List getDefaultMapping(String streamKey) { - return getStreamFieldsInfo(streamKey, null) + private List getDefaultMapping(String streamKey, String tbId) { + return getStreamFieldsInfo(streamKey, null, tbId) .stream() .map(streamFieldInfo -> new FieldToColumnMapping(streamFieldInfo, null)) .collect(Collectors.toList()); } - private String[] getStreamSymbols(String streamKey) { - DXTickStream stream = getStream(streamKey); + private String[] getStreamSymbols(String streamKey, String tbId) { + DXTickStream stream = getStream(streamKey, tbId); return Arrays.stream(stream.listEntities()) .map(IdentityKey::getSymbol) .map(CharSequence::toString) @@ -228,18 +230,20 @@ private String[] getStreamSymbols(String streamKey) { @Override public List getMappings(String id, CsvImportGeneralSettings settings) { + String tbId = getImportById(id).getTbId(); try { - return getInitMappings(id, settings); + return getInitMappings(id, settings, tbId); } catch (Exception e) { - return getDefaultMapping(settings.getStreamKey()); + return getDefaultMapping(settings.getStreamKey(), tbId); } } @Override public List validateMapping(CsvImportSettings settings, String id) { + String tbId = getImportById(id).getTbId(); ImportValidator validator = createImportValidator(settings, id); Set usedTypes = settings.getGeneralSettings().getTypeToKeywordMapping().keySet(); - Set usedFields = getStreamFieldsInfo(settings.getGeneralSettings().getStreamKey(), usedTypes); + Set usedFields = getStreamFieldsInfo(settings.getGeneralSettings().getStreamKey(), usedTypes, tbId); return validator.validateMapping(usedFields); } @@ -359,7 +363,10 @@ public synchronized void startImport(String id, SubscriptionChannel channel) { ImportProcessReport report = createImportReporterWithWriter(processId, channel); executorService.submit(() -> { try { - ImportTask task = new ImportDirectoryTask(timebaseService, importProcess, report, settings, status); + String tbId = importData.getTbId(); + ImportTask task = new ImportDirectoryTask( + registry.resolve(tbId), + importProcess, report, settings, status); importProcess.importTask(task); LOGGER.info().append("Start CSV import process id: ").append(processId).commit(); while (!importProcess.ready()) { @@ -556,9 +563,9 @@ private String getTimestampHeader(List initMappings) { .orElse("timestamp"); } - private List getInitMappings(String id, CsvImportGeneralSettings settings) { + private List getInitMappings(String id, CsvImportGeneralSettings settings, String tbId) { Map> csvImportData = getPreviewParseValues(id, settings.getSeparator(), settings.getCharset()); - Set usedFields = getStreamFieldsInfo(settings.getStreamKey(), settings.getTypeToKeywordMapping().keySet()); + Set usedFields = getStreamFieldsInfo(settings.getStreamKey(), settings.getTypeToKeywordMapping().keySet(), tbId); return getStreamFieldToColumnNameMapping(usedFields, csvImportData); } @@ -575,8 +582,8 @@ private Map getPreviewMap(String id) { return getImportById(id).getCsvPreviewData(); } - private Map getTypeMappings(String streamKey) { - DXTickStream stream = getStream(streamKey); + private Map getTypeMappings(String streamKey, String tbId) { + DXTickStream stream = getStream(streamKey, tbId); RecordClassDescriptor[] descriptors = TickDBShell.collectTypes(stream); Map typeMappings = new HashMap<>(); for (RecordClassDescriptor descriptor : descriptors) { @@ -678,8 +685,8 @@ private DirectoryImportProcess getDirectoryImportProcess(long id) { return (DirectoryImportProcess) importProcess; } - private Set getStreamFieldsInfo(String streamKey, Set typesFilter) { - DXTickStream stream = getStream(streamKey); + private Set getStreamFieldsInfo(String streamKey, Set typesFilter, String tbId) { + DXTickStream stream = getStream(streamKey, tbId); RecordClassDescriptor[] descriptors = TickDBShell.collectTypes(stream); Set streamFields = new HashSet<>(); for (RecordClassDescriptor descriptor : descriptors) { @@ -728,8 +735,8 @@ private CsvImportData findImportByProcessId(long processId) { return null; } - private DXTickStream getStream(String streamName) { - DXTickStream stream = TBWGUtils.getStream(timebaseService, streamName); + private DXTickStream getStream(String streamName, String tbId) { + DXTickStream stream = TBWGUtils.getStream(registry.resolve(tbId), streamName); if (stream == null) { throw new IllegalArgumentException(streamName + " stream not found."); } @@ -738,7 +745,8 @@ private DXTickStream getStream(String streamName) { private ImportValidator createImportValidator(CsvImportSettings settings, String id) { Map previewMap = getPreviewMap(id); - DXTickStream stream = getStream(settings.getGeneralSettings().getStreamKey()); + String tbId = getImportById(id).getTbId(); + DXTickStream stream = getStream(settings.getGeneralSettings().getStreamKey(), tbId); RecordClassDescriptor[] descriptors = TickDBShell.collectTypes(stream); return new ImportValidatorCsv(descriptors, settings, previewMap); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/ImportServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/ImportServiceImpl.java index fad606f2..fe94c3c5 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/ImportServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/ImportServiceImpl.java @@ -24,7 +24,7 @@ import com.epam.deltix.qsrv.hf.pub.md.json.SchemaDef; import com.epam.deltix.qsrv.hf.stream.MessageReader2; import com.epam.deltix.qsrv.hf.tickdb.pub.DXTickStream; -import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.csvimport.ImportProcessReport; import com.epam.deltix.tbwg.webapp.services.timebase.csvimport.ImportStatus; import com.epam.deltix.tbwg.webapp.services.view.utils.Utils; @@ -48,13 +48,13 @@ public class ImportServiceImpl implements ImportService { private static final Log LOGGER = LogFactory.getLog(ImportServiceImpl.class); - private final TimebaseService timebaseService; + private final TimebaseRegistry registry; private final UploadFileService uploadFileService; private final ExecutorService executorService = Executors.newCachedThreadPool(); private final ImportStatusService statusService; - public ImportServiceImpl(TimebaseService timebaseService, UploadFileService uploadFileService, ImportStatusService statusService) { - this.timebaseService = timebaseService; + public ImportServiceImpl(TimebaseRegistry registry, UploadFileService uploadFileService, ImportStatusService statusService) { + this.registry = registry; this.uploadFileService = uploadFileService; this.statusService = statusService; } @@ -108,7 +108,10 @@ public SchemaDef getSchema(long id) { @Override public boolean isValidImportSchema(long id, String streamKey) { - DXTickStream stream = timebaseService.getStream(streamKey); + ImportProcess process = uploadFileService.uploadProcess(id); + String tbId = process instanceof FileImportProcess ? + ((FileImportProcess) process).importSettings().getTbId() : null; + DXTickStream stream = registry.resolve(tbId).getStream(streamKey); if (stream == null) { return true; } @@ -182,9 +185,12 @@ public void startImport(long id, SubscriptionChannel channel) { } FileImportProcess fileImportProcess = (FileImportProcess) importProcess; ImportStatus status = statusService.newImportStatus(id); + String tbId = fileImportProcess.importSettings().getTbId(); executorService.submit(() -> { try { - ImportFileTask task = new ImportFileTask(timebaseService, channel, fileImportProcess, status); + ImportFileTask task = new ImportFileTask( + registry.resolve(tbId), + channel, fileImportProcess, status); fileImportProcess.importTask(task); while (!importProcess.ready()) { if (task.isCancelled()) { diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/ImportSettings.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/ImportSettings.java index 6f153be2..b7757267 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/ImportSettings.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/ImportSettings.java @@ -31,4 +31,6 @@ public interface ImportSettings { Periodicity getPeriodicity(); boolean isFileBySymbol(); LoadingOptions.WriteMode getWriteMode(); + + default String getTbId() { return null; } } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/QmsgImportSettings.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/QmsgImportSettings.java index 88bff011..4c5805d6 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/QmsgImportSettings.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/export/imp/QmsgImportSettings.java @@ -36,4 +36,5 @@ public class QmsgImportSettings implements ImportSettings { private Periodicity periodicity; private boolean fileBySymbol; private LoadingOptions.WriteMode writeMode; + private String tbId; } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/playback/PlaybackConfig.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/playback/PlaybackConfig.java index d9197c34..b7376d43 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/playback/PlaybackConfig.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/playback/PlaybackConfig.java @@ -31,5 +31,7 @@ public class PlaybackConfig { private boolean cyclic = false; private boolean targetTopic = false; private boolean permanent = false; + private String sourceTb; + private String targetTb; } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/playback/PlaybackServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/playback/PlaybackServiceImpl.java index 8f186c74..c9767a9d 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/playback/PlaybackServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/timebase/playback/PlaybackServiceImpl.java @@ -30,6 +30,7 @@ import com.epam.deltix.qsrv.hf.tickdb.ui.tbshell.TickDBShell; import com.epam.deltix.streaming.MessageChannel; import com.epam.deltix.streaming.MessageSource; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.timebase.exc.NoStreamsException; import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; @@ -37,7 +38,6 @@ import com.epam.deltix.timebase.messages.InstrumentMessage; import com.epam.deltix.util.lang.Util; import com.epam.deltix.util.time.TimeKeeper; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -56,9 +56,6 @@ public class PlaybackServiceImpl implements PlaybackService { private final static AtomicLong ID_GENERATOR = new AtomicLong(System.currentTimeMillis()); - @Autowired - private TimebaseService timebaseService; - private final Map players = new HashMap<>(); private final Map playerToUser = new HashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); @@ -68,12 +65,20 @@ public class PlaybackServiceImpl implements PlaybackService { @Value("${playback.close-delay:60}") private long closeDelaySec; + private final TimebaseRegistry registry; + + public PlaybackServiceImpl(TimebaseRegistry registry) { + this.registry = registry; + } + @Override public long createPlayer(PlaybackConfig config, String userName) throws NoStreamsException { + TimebaseService sourceService = registry.resolve(config.getSourceTb()); + TimebaseService targetService = registry.resolve(config.getTargetTb()); synchronized (players) { checkSize(); long id = ID_GENERATOR.incrementAndGet(); - PlaybackPlayer player = createNewPlayer(config); + PlaybackPlayer player = createNewPlayer(config, sourceService, targetService); players.put(id, player); playerToUser.put(id, userName); listeners.forEach(l -> l.playbackCreated(id)); @@ -121,22 +126,22 @@ private void checkSize() { } } - private PlaybackPlayer createNewPlayer(PlaybackConfig config) throws NoStreamsException { - return createRealtimePlayer(config); + private PlaybackPlayer createNewPlayer(PlaybackConfig config, TimebaseService sourceService, TimebaseService targetService) throws NoStreamsException { + return createRealtimePlayer(config, sourceService, targetService); } - private PlaybackPlayer createRealtimePlayer(PlaybackConfig config) throws NoStreamsException { + private PlaybackPlayer createRealtimePlayer(PlaybackConfig config, TimebaseService sourceService, TimebaseService targetService) throws NoStreamsException { - DXTickStream[] sourceStreams = TBWGUtils.match(timebaseService, config.getSourceStreams()); + DXTickStream[] sourceStreams = TBWGUtils.match(sourceService, config.getSourceStreams()); if (sourceStreams == null) { throw new NoStreamsException(config.getSourceStreams()); } DXChannel targetChannel; try { if (config.isTargetTopic()) { - targetChannel = getOrCreateTopic(config.getTargetStream(), sourceStreams); + targetChannel = getOrCreateTopic(targetService, config.getTargetStream(), sourceStreams); } else { - targetChannel = getOrCreateStream(config.getTargetStream(), sourceStreams); + targetChannel = getOrCreateStream(targetService, config.getTargetStream(), sourceStreams); } } catch (Exception e) { throw new IllegalArgumentException("Can't create playback destination. Reason: " + e.getMessage(), e); @@ -156,7 +161,7 @@ private PlaybackPlayer createRealtimePlayer(PlaybackConfig config) throws NoStre LoadingOptions rawLoaderOptions = LoadingOptions.withRewriteMode(true); SelectionOptions selectionOptions = new SelectionOptions(true, false); - DXTickDB connection = timebaseService.getConnection(); + DXTickDB connection = sourceService.getConnection(); final InstrumentMessageSource finalCur = connection.select(startTime, selectionOptions, sourceStreams); cur = finalCur; if (config.isTargetTopic()) { @@ -194,8 +199,8 @@ private PlaybackPlayer createRealtimePlayer(PlaybackConfig config) throws NoStre return new RealtimePlayer(playerExecutor, config); } - private DXChannel getOrCreateTopic(String key, DXTickStream[] sourceStreams) { - TopicDB db = timebaseService.getTopicDB(); + private DXChannel getOrCreateTopic(TimebaseService service, String key, DXTickStream[] sourceStreams) { + TopicDB db = service.getTopicDB(); DirectChannel topic = db.getTopic(key); if (topic == null) { RecordClassDescriptor[] types = mergeStreamsSchema(sourceStreams); @@ -204,8 +209,8 @@ private DXChannel getOrCreateTopic(String key, DXTickStream[] sourceStreams) { return topic; } - private DXTickStream getOrCreateStream(String targetStreamName, StreamOptions options) { - DXTickDB db = timebaseService.getConnection(); + private DXTickStream getOrCreateStream(TimebaseService service, String targetStreamName, StreamOptions options) { + DXTickDB db = service.getConnection(); DXTickStream stream = db.getStream(targetStreamName); if (stream == null) { options.version = null; @@ -215,11 +220,11 @@ private DXTickStream getOrCreateStream(String targetStreamName, StreamOptions op return stream; } - private DXTickStream getOrCreateStream(String targetStreamName, DXTickStream[] streams) { + private DXTickStream getOrCreateStream(TimebaseService service, String targetStreamName, DXTickStream[] streams) { if (streams.length == 1) { - return getOrCreateStream(targetStreamName, streams[0].getStreamOptions()); + return getOrCreateStream(service, targetStreamName, streams[0].getStreamOptions()); } - DXTickDB db = timebaseService.getConnection(); + DXTickDB db = service.getConnection(); DXTickStream stream = db.getStream(targetStreamName); if (stream == null) { StreamOptions options = new StreamOptions(StreamScope.DURABLE, targetStreamName, null, 1); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/topic/TopicServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/topic/TopicServiceImpl.java index 61f0c414..6fa160f8 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/topic/TopicServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/topic/TopicServiceImpl.java @@ -23,7 +23,7 @@ import com.epam.deltix.qsrv.hf.tickdb.pub.topic.settings.TopicSettings; import com.epam.deltix.tbwg.webapp.model.tree.TreeNodeDef; import com.epam.deltix.tbwg.webapp.model.tree.TreeNodeType; -import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.tree.TreeFilter; import com.epam.deltix.tbwg.webapp.utils.TBWGUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -37,42 +37,42 @@ public class TopicServiceImpl implements TopicService { - private final TimebaseService timebaseService; + private final TimebaseRegistry registry; private final List listeners = new CopyOnWriteArrayList<>(); @Autowired - public TopicServiceImpl(TimebaseService timebaseService) { - this.timebaseService = timebaseService; + public TopicServiceImpl(TimebaseRegistry registry) { + this.registry = registry; } @Override public List listTopics() { - return timebaseService.getTopicDB().listTopics(); + return registry.getDefault().getTopicDB().listTopics(); } @Override public DirectChannel createTopic(String key, RecordClassDescriptor[] types, TopicSettings settings) { String copyToStream = settings.getCopyToStream(); if (copyToStream != null) { - DXTickStream stream = timebaseService.getStream(copyToStream); + DXTickStream stream = registry.getDefault().getStream(copyToStream); if (stream == null) { TBWGUtils.validateStreamKey(copyToStream); - timebaseService.getOrCreateStream(copyToStream, (options) -> { }, types); + registry.getDefault().getOrCreateStream(copyToStream, (options) -> { }, types); } else { if (!SchemaMergeHelper.containsAll(stream.getTypes(), types)) { throw new IllegalArgumentException("Duplicate stream scheme is not comparable with the topic scheme"); } } } - DirectChannel topic = timebaseService.getTopicDB().createTopic(key, types, settings); + DirectChannel topic = registry.getDefault().getTopicDB().createTopic(key, types, settings); listeners.forEach(l -> l.topicCreated(key)); return topic; } @Override public TreeNodeDef getStructure(TreeFilter treeFilter) { - TreeNodeDef structure = new TreeNodeDef(timebaseService.getId(), timebaseService.getId(), TreeNodeType.TOPIC); + TreeNodeDef structure = new TreeNodeDef(registry.getDefault().getId(), registry.getDefault().getId(), TreeNodeType.TOPIC); List listTopics = listTopics(); if (treeFilter != null) { listTopics = listTopics.stream().filter(treeFilter::test).collect(Collectors.toList()); @@ -88,13 +88,13 @@ public TreeNodeDef getStructure(TreeFilter treeFilter) { @Override public void delete(String key) { - timebaseService.getTopicDB().deleteTopic(key); + registry.getDefault().getTopicDB().deleteTopic(key); listeners.forEach(l -> l.topicDeleted(key)); } @Override public RecordClassDescriptor[] getTypes(String key) { - return timebaseService.getTopicDB().getTypes(key); + return registry.getDefault().getTopicDB().getTypes(key); } @Override diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TickDbTreeNode.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TickDbTreeNode.java index 6f183589..d358baaa 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TickDbTreeNode.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TickDbTreeNode.java @@ -78,7 +78,11 @@ public TickDbTreeNode(TreeConfig config, TimebaseService db, ViewService viewSer List dbContent; if (config.isViews()) { - dbContent = viewService.list().stream() + if (!db.isConnected()) { + String error = db.getLastError(); + throw new RuntimeException(error != null ? error : "Timebase is unavailable"); + } + dbContent = viewService.list(db.getId()).stream() .map(v -> { try { DXTickStream stream = db.getStream(v.getStream()); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TimeBaseTreeService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TimeBaseTreeService.java index f24056d8..2c6e9f8f 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TimeBaseTreeService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TimeBaseTreeService.java @@ -24,5 +24,5 @@ public interface TimeBaseTreeService { TreeNodeDef buildTree(List paths, TreeFilter filter, boolean showSpaces, boolean views, boolean filterRootOnly); - TreeNodeDef findSymbolTree(String stream, String symbol, boolean showSpaces, boolean views); + TreeNodeDef findSymbolTree(String tbId, String stream, String symbol, boolean showSpaces, boolean views); } \ No newline at end of file diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TimeBaseTreeServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TimeBaseTreeServiceImpl.java index 74a3f93b..55381926 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TimeBaseTreeServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/tree/TimeBaseTreeServiceImpl.java @@ -16,12 +16,16 @@ */ package com.epam.deltix.tbwg.webapp.services.tree; +import com.epam.deltix.gflog.api.Log; +import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.qsrv.hf.tickdb.pub.DBStateListener; import com.epam.deltix.qsrv.hf.tickdb.pub.DBStateNotifier; import com.epam.deltix.qsrv.hf.tickdb.pub.DXTickDB; import com.epam.deltix.tbwg.webapp.events.TimeBaseEvent; import com.epam.deltix.tbwg.webapp.model.tree.TreeNodeDef; +import com.epam.deltix.tbwg.webapp.model.tree.TreeNodeType; import com.epam.deltix.tbwg.webapp.services.timebase.SystemMessagesService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.view.ViewService; import com.epam.deltix.tbwg.webapp.settings.TimeBaseTreeSettings; @@ -38,6 +42,8 @@ @Service public class TimeBaseTreeServiceImpl implements TimeBaseTreeService, DBStateListener, ApplicationListener { + private static final Log LOGGER = LogFactory.getLog(TimeBaseTreeServiceImpl.class); + private static class TreePath { private final String[] elements; @@ -65,27 +71,35 @@ public boolean isLast(int depth) { } private final TimeBaseTreeSettings settings; - private final TimebaseService timebaseService; + private final TimebaseRegistry registry; private final ViewService viewService; + private final SystemMessagesService messagesService; private final SplitGroupsStrategy splitGroupsStrategy = new BucketSplitGroups(); private final SpaceEntitiesCache spaceEntitiesCache; public TimeBaseTreeServiceImpl(TimeBaseTreeSettings settings, - TimebaseService timebaseService, ViewService viewService, + TimebaseRegistry registry, ViewService viewService, SystemMessagesService messagesService) { this.settings = settings; - this.timebaseService = timebaseService; + this.registry = registry; this.viewService = viewService; + this.messagesService = messagesService; this.spaceEntitiesCache = new SpaceEntitiesCacheImpl(); messagesService.masterNotifier().subscribe(this); } @PreDestroy public void destroy() { - DXTickDB db = timebaseService.getConnection(); - if (db instanceof DBStateNotifier) { - ((DBStateNotifier) db).removeStateListener(this); + messagesService.masterNotifier().unsubscribe(this); + for (TimebaseService svc : registry.getAll()) { + try { + DXTickDB db = svc.getConnection(); + if (db instanceof DBStateNotifier) { + ((DBStateNotifier) db).removeStateListener(this); + } + } catch (Exception ignored) { + } } } @@ -120,24 +134,73 @@ public void renamed(String fromKey, String toKey) { @Override public TreeNodeDef buildTree(List paths, TreeFilter filter, boolean showSpaces, boolean views, boolean filterRootOnly) { - List treePaths = paths.stream() - .map(TreePath::new) - .collect(Collectors.toList()); + List allServices = registry.getAll(); + + if (allServices.size() == 1) { + List treePaths = paths.stream().map(TreePath::new).collect(Collectors.toList()); + return walk( + new TickDbTreeNode( + new TreeConfig(filter, showSpaces, views, filterRootOnly, settings, splitGroupsStrategy, spaceEntitiesCache), + allServices.get(0), viewService + ), + treePaths, 1 + ).getTreeNodeDef(); + } + + // Multi-instance: virtual ROOT node, one DB child per TB. + // Paths start with /{tbId}/... or "/" to expand everything. + TreeNodeDef root = new TreeNodeDef("", "", TreeNodeType.ROOT); + for (TimebaseService tb : allServices) { + String tbId = tb.getId(); + TreeNodeDef dbDef; + try { + TreeConfig config = new TreeConfig(filter, showSpaces, views, filterRootOnly, settings, splitGroupsStrategy, spaceEntitiesCache); + TickDbTreeNode dbNode = new TickDbTreeNode(config, tb, viewService); + + List tbPaths = paths.stream() + .filter(p -> p.equals("/") || isPathForTb(p, tbId)) + .map(p -> p.equals("/") ? "/" : stripTbPrefix(p, tbId)) + .map(TreePath::new) + .collect(Collectors.toList()); + dbDef = tbPaths.isEmpty() ? dbNode.getTreeNodeDef() : walk(dbNode, tbPaths, 1).getTreeNodeDef(); + } catch (Exception e) { + LOGGER.warn().append("Timebase [").append(tbId).append("] is unavailable, showing it as an errored node in the tree: ") + .append(e.getMessage()).commit(); + dbDef = new TreeNodeDef(tbId, tbId, TreeNodeType.DB); + dbDef.setAvailable(false); + dbDef.setErrorMessage(describeError(e)); + } + + root.getChildren().add(dbDef); + } + + root.setChildrenCount(root.getChildren().size()); + root.setTotalCount(root.getChildren().size()); + return root; + } + + private static String describeError(Throwable e) { + return e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName(); + } + + private static boolean isPathForTb(String path, String tbId) { + return path.equals("/" + tbId) || path.startsWith("/" + tbId + "/"); + } - return walk( - new TickDbTreeNode( - new TreeConfig(filter, showSpaces, views, filterRootOnly, settings, splitGroupsStrategy, spaceEntitiesCache), - timebaseService, viewService - ), - treePaths, 1 - ).getTreeNodeDef(); + private static String stripTbPrefix(String path, String tbId) { + String prefix = "/" + tbId; + if (path.equals(prefix)) { + return "/"; + } + return path.substring(prefix.length()); // "/tb1/streamA" → "/streamA" } @Override - public TreeNodeDef findSymbolTree(String stream, String symbol, boolean showSpaces, boolean views) { + public TreeNodeDef findSymbolTree(String tbId, String stream, String symbol, boolean showSpaces, boolean views) { + TimebaseService tb = registry.resolve(tbId); TickDbTreeNode dbTreeNode = new TickDbTreeNode( new TreeConfig(null, showSpaces, views, false, settings, splitGroupsStrategy, spaceEntitiesCache), - timebaseService, viewService + tb, viewService ); TreeNode node = dbTreeNode.addChild(stream); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/ViewService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/ViewService.java index 8b33e871..4a95308f 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/ViewService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/ViewService.java @@ -16,6 +16,7 @@ */ package com.epam.deltix.tbwg.webapp.services.view; +import com.epam.deltix.tbwg.webapp.services.timebase.exc.InvalidQueryException; import com.epam.deltix.tbwg.webapp.services.view.md.ViewMd; import java.time.Instant; @@ -33,17 +34,17 @@ static String getStreamName(String id) { boolean isViewStream(String key); - void create(ViewMd viewMd); + void create(ViewMd viewMd, String tbId) throws InvalidQueryException; - void delete(String id); + void delete(String id, String tbId); - void restart(String id, Instant from); + void restart(String id, String tbId, Instant from); - void stop(String id); + void stop(String id, String tbId); - ViewMd get(String id); + ViewMd get(String id, String tbId); - List list(); + List list(String tbId); void subscribe(ViewListener listener); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/ViewServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/ViewServiceImpl.java index 93a6106b..ace45f05 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/ViewServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/ViewServiceImpl.java @@ -18,9 +18,16 @@ import com.epam.deltix.gflog.api.Log; import com.epam.deltix.gflog.api.LogFactory; +import com.epam.deltix.qsrv.hf.pub.md.ClassDescriptor; +import com.epam.deltix.qsrv.hf.pub.md.ClassSet; import com.epam.deltix.qsrv.hf.tickdb.pub.DXTickStream; +import com.epam.deltix.qsrv.hf.tickdb.pub.SelectionOptions; import com.epam.deltix.tbwg.messages.ViewState; +import com.epam.deltix.tbwg.webapp.services.timebase.exc.InvalidQueryException; +import com.epam.deltix.tbwg.webapp.services.view.md.QueryViewMd; +import com.epam.deltix.util.parsers.CompilationException; import com.epam.deltix.tbwg.webapp.services.tasks.workers.FixedSizeWorkersManager; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.view.md.MutableQueryViewMd; import com.epam.deltix.tbwg.webapp.services.view.md.MutableViewMd; @@ -36,8 +43,12 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import java.time.Instant; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; @Service public class ViewServiceImpl implements ViewService, ViewProcessingListener, ViewMdEventsListener { @@ -47,39 +58,40 @@ public class ViewServiceImpl implements ViewService, ViewProcessingListener, Vie @Value("${views.processor.thread-pool-size:4}") private int threadPoolSize; - private final TimebaseService timebaseService; - private final ViewMdRepository mdRepository; - private final ViewMdEventsPublisher mdEventsPublisher; - private final TimebaseViewMdCache timebaseViewMdCache; + private final TimebaseRegistry registry; + private final Map caches = new LinkedHashMap<>(); private ViewMdProcessorImpl processor; private FixedSizeWorkersManager workersManager; private final List listeners = new CopyOnWriteArrayList<>(); - public ViewServiceImpl(TimebaseService timebaseService) { - this.timebaseService = timebaseService; - this.timebaseViewMdCache = new TimebaseViewMdCache(timebaseService); - this.mdRepository = timebaseViewMdCache; - this.mdEventsPublisher = timebaseViewMdCache; + public ViewServiceImpl(TimebaseRegistry registry) { + this.registry = registry; + for (TimebaseService tb : registry.getAll()) { + caches.put(tb.getId(), new TimebaseViewMdCache(tb)); + } } @PostConstruct public void init() { this.workersManager = new FixedSizeWorkersManager(threadPoolSize); - this.processor = new ViewMdProcessorImpl(timebaseService, this, workersManager); + this.processor = new ViewMdProcessorImpl(registry, this, workersManager); - this.mdEventsPublisher.subscribe(processor); - this.mdEventsPublisher.subscribe(this); - timebaseViewMdCache.start(); + for (TimebaseViewMdCache cache : caches.values()) { + cache.subscribe(processor); + cache.subscribe(this); + cache.start(); + } } @PreDestroy public void destroy() { - mdEventsPublisher.unsubscribe(processor); - mdEventsPublisher.unsubscribe(this); - - timebaseViewMdCache.stop(); + for (TimebaseViewMdCache cache : caches.values()) { + cache.unsubscribe(processor); + cache.unsubscribe(this); + cache.stop(); + } processor.stop(); workersManager.close(); } @@ -91,29 +103,49 @@ public void refresh() { @Override public boolean isViewStream(String key) { - if (!mdRepository.isInitialized()) { + if (!isAnyInitialized()) { return false; } if (key != null && key.endsWith(VIEW_STREAM_SUFFIX)) { - return mdRepository.findById(getIdByKey(key)) != null; + return findAcrossAll(getIdByKey(key)) != null; } return false; } @Override - public synchronized void create(ViewMd viewMd) { - ViewMd savedMd = mdRepository.findById(viewMd.getId()); - if (savedMd != null) { + public synchronized void create(ViewMd viewMd, String tbId) throws InvalidQueryException { + if (findAcrossAll(viewMd.getId()) != null) { throw new IllegalArgumentException("View with id " + viewMd.getId() + " already exists"); } - mdRepository.saveAll(viewMd); + TimebaseService tb = registry.resolve(tbId); + if (viewMd instanceof QueryViewMd) { + String query = ((QueryViewMd) viewMd).getQuery(); + try { + ClassSet classSet = tb.getConnection().describeQuery(query, new SelectionOptions()); + ClassDescriptor[] descriptors = classSet.getClasses(); + for (ClassDescriptor descriptor : descriptors) { + if (descriptor.getName() == null) { + throw new NullPointerException("Query result set contains types with empty name. " + + "Use `TYPE` keyword to specify type name for result set, for example: `SELECT a, b, c TYPE MyType`"); + } + } + } catch (CompilationException e) { + throw new InvalidQueryException(query); + } + } + + if (viewMd instanceof MutableViewMd) { + ((MutableViewMd) viewMd).setTbId(tb.getId()); + } + + findCache(tb.getId()).saveAll(viewMd); } @Override - public synchronized void restart(String viewId, Instant from) { - ViewMd savedMd = mdRepository.findById(viewId); + public synchronized void restart(String viewId, String tbId, Instant from) { + ViewMd savedMd = findViewMd(viewId, tbId); if (savedMd == null) { throw new IllegalArgumentException("View with id " + viewId + " doesn't exist"); } @@ -123,15 +155,15 @@ public synchronized void restart(String viewId, Instant from) { queryMd.setInfo(null); queryMd.setState(ViewState.RESTARTED); queryMd.setLastTimestamp(from != null ? from.toEpochMilli() : Long.MIN_VALUE); - mdRepository.saveAll(queryMd); + findCache(savedMd.getTbId()).saveAll(queryMd); } else { throw new RuntimeException("Invalid view metadata type"); } } @Override - public synchronized void stop(String viewId) { - ViewMd savedMd = mdRepository.findById(viewId); + public synchronized void stop(String viewId, String tbId) { + ViewMd savedMd = findViewMd(viewId, tbId); if (savedMd == null) { throw new IllegalArgumentException("View with id " + viewId + " doesn't exist"); } @@ -140,42 +172,54 @@ public synchronized void stop(String viewId) { MutableQueryViewMd queryMd = (MutableQueryViewMd) savedMd; queryMd.setInfo(null); queryMd.setState(ViewState.STOPPED); - mdRepository.saveAll(queryMd); + findCache(savedMd.getTbId()).saveAll(queryMd); } else { throw new RuntimeException("Invalid view metadata type"); } } @Override - public synchronized ViewMd get(String id) { - return mdRepository.findById(id); + public synchronized ViewMd get(String id, String tbId) { + return findViewMd(id, tbId); } @Override - public synchronized List list() { - return mdRepository.findAll(); + public synchronized List list(String tbId) { + if (tbId != null && !tbId.isEmpty()) { + TimebaseViewMdCache cache = caches.get(tbId); + if (cache == null) return Collections.emptyList(); + if (!cache.isInitialized()) { + LOGGER.warn().append("[").append(tbId).append("] View md cache not yet initialized, returning empty list").commit(); + return Collections.emptyList(); + } + return cache.findAll(); + } + return caches.values().stream() + .filter(TimebaseViewMdCache::isInitialized) + .flatMap(c -> c.findAll().stream()) + .collect(Collectors.toList()); } @Override - public synchronized void delete(String viewId) { - ViewMd viewMd = mdRepository.findById(viewId); + public synchronized void delete(String viewId, String tbId) { + ViewMd viewMd = findViewMd(viewId, tbId); if (viewMd == null) { throw new IllegalArgumentException("View with id " + viewId + " doesn't exist"); } - mdRepository.delete(viewMd); + findCache(viewMd.getTbId()).delete(viewMd); } @Override public void onUpdate(ViewProcessingEvent event) { - ViewMd viewMd = mdRepository.findById(event.getViewId()); + ViewMd viewMd = findAcrossAll(event.getViewId()); if (viewMd instanceof MutableViewMd) { MutableViewMd mutableMd = (MutableViewMd) viewMd; mutableMd.setState(event.getState()); mutableMd.setLastTimestamp(event.getLastTimestamp()); mutableMd.setInfo(event.getReason()); - mdRepository.saveAll(mutableMd); + findCache(mutableMd.getTbId()).saveAll(mutableMd); } } @@ -201,7 +245,7 @@ public void created(ViewMd viewMd) { @Override public void removed(ViewMd viewMd) { listeners.forEach(l -> l.deleted(viewMd)); - deleteStream(viewMd.getStream()); + deleteStream(viewMd); } @Override @@ -209,9 +253,51 @@ public void updated(ViewMd viewMd) { listeners.forEach(l -> l.updated(viewMd)); } - private void deleteStream(String streamKey) { + private ViewMd findViewMd(String id, String tbId) { + if (tbId != null && !tbId.isEmpty()) { + TimebaseViewMdCache cache = caches.get(tbId); + if (cache != null) { + try { + ViewMd found = cache.findById(id); + if (found != null) return found; + } catch (RuntimeException ignored) {} + } + } + return findAcrossAll(id); + } + + private ViewMd findAcrossAll(String id) { + for (TimebaseViewMdCache cache : caches.values()) { + try { + ViewMd found = cache.findById(id); + if (found != null) return found; + } catch (RuntimeException ignored) { + // cache not yet initialized + } + } + return null; + } + + private TimebaseViewMdCache findCache(String tbId) { + TimebaseViewMdCache cache = caches.get(tbId); + if (cache == null) { + // fall back to default + cache = caches.get(registry.getDefault().getId()); + } + if (cache == null) { + throw new IllegalStateException("No view cache available"); + } + return cache; + } + + private boolean isAnyInitialized() { + return caches.values().stream().anyMatch(TimebaseViewMdCache::isInitialized); + } + + private void deleteStream(ViewMd viewMd) { try { - DXTickStream stream = timebaseService.getStream(streamKey); + TimebaseService tb = registry.resolve(viewMd.getTbId()); + DXTickStream stream = tb.getStream(viewMd.getStream()); if (stream != null) { stream.delete(); } @@ -221,7 +307,7 @@ private void deleteStream(String streamKey) { } private String getIdByKey(String key) { - return key.substring(0, key.length() - VIEW_STREAM_SUFFIX.length()); + return key.substring(0, key.length() - VIEW_STREAM_SUFFIX.length()); } -} \ No newline at end of file +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/MutableViewMd.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/MutableViewMd.java index 585ee5a5..55e2fe29 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/MutableViewMd.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/MutableViewMd.java @@ -23,6 +23,8 @@ public interface MutableViewMd extends ViewMd { void setId(String id); + void setTbId(String tbId); + void setTimestamp(long timestamp); void setStream(String stream); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMd.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMd.java index cebcb969..30fb913f 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMd.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMd.java @@ -25,6 +25,8 @@ public interface ViewMd { String getId(); + String getTbId(); + long getTimestamp(); String getStream(); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMdImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMdImpl.java index a40abcb9..cf136a98 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMdImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMdImpl.java @@ -27,6 +27,7 @@ abstract class ViewMdImpl implements MutableViewMd { private String id; + private String tbId; private long timestamp = Long.MIN_VALUE; private String stream; private boolean live; @@ -45,6 +46,16 @@ public void setId(String id) { this.id = id; } + @Override + public String getTbId() { + return tbId; + } + + @Override + public void setTbId(String tbId) { + this.tbId = tbId; + } + @Override public long getTimestamp() { return timestamp; diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMdUtils.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMdUtils.java index a3550769..9cf126f8 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMdUtils.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/ViewMdUtils.java @@ -27,10 +27,11 @@ public MutableQueryViewMd newQueryViewInfo() { return new QueryViewMdImpl(); } - public ViewMd fromMessage(ViewMetadataMessage message) { + public ViewMd fromMessage(ViewMetadataMessage message, String tbId) { QueryViewMdImpl viewMd = new QueryViewMdImpl(); viewMd.setQuery(toString(message.getQuery())); viewMd.setId(toString(message.getSymbol())); + viewMd.setTbId(tbId); viewMd.setTimestamp(message.getTimeStampMs()); viewMd.setLastTimestamp(message.getLastTimestamp()); viewMd.setStream(toString(message.getOutput())); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/repository/TimebaseViewMdCache.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/repository/TimebaseViewMdCache.java index 673885e0..aebc10ad 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/repository/TimebaseViewMdCache.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/md/repository/TimebaseViewMdCache.java @@ -18,13 +18,7 @@ import com.epam.deltix.gflog.api.Log; import com.epam.deltix.gflog.api.LogFactory; -import com.epam.deltix.qsrv.hf.pub.md.RecordClassSet; import com.epam.deltix.qsrv.hf.tickdb.pub.*; -import com.epam.deltix.qsrv.hf.tickdb.pub.lock.DBLock; -import com.epam.deltix.qsrv.hf.tickdb.pub.task.SchemaChangeTask; -import com.epam.deltix.qsrv.hf.tickdb.schema.*; -import com.epam.deltix.tbwg.messages.QueryViewMdMessage; -import com.epam.deltix.tbwg.messages.ViewMdMessage; import com.epam.deltix.tbwg.messages.ViewMetadataMessage; import com.epam.deltix.tbwg.messages.ViewState; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; @@ -32,7 +26,6 @@ import com.epam.deltix.tbwg.webapp.services.view.md.MutableViewMd; import com.epam.deltix.tbwg.webapp.services.view.md.ViewMd; import com.epam.deltix.tbwg.webapp.services.view.md.ViewMdUtils; -import com.epam.deltix.tbwg.webapp.utils.TimeBaseUtils; import com.epam.deltix.timebase.messages.ConstantIdentityKey; import com.epam.deltix.timebase.messages.IdentityKey; import com.epam.deltix.timebase.messages.InstrumentMessage; @@ -55,6 +48,7 @@ public class TimebaseViewMdCache implements ViewMdRepository, ViewMdEventsPublis private static final long SYMBOL_HISTORY_MESSAGE_COUNT = 100; private final TimebaseService timebaseService; + private final String tbId; private final ExecutorService processEventsExecutor = Executors.newSingleThreadExecutor(); @@ -69,6 +63,7 @@ public class TimebaseViewMdCache implements ViewMdRepository, ViewMdEventsPublis public TimebaseViewMdCache(TimebaseService timebaseService) { this.timebaseService = timebaseService; + this.tbId = timebaseService.getId(); } public void start() { @@ -76,20 +71,20 @@ public void start() { } public void stop() { - LOGGER.info().append("Stream view# processing stop").commit(); + LOGGER.info().append("[").append(tbId).append("] Stream view# processing stop").commit(); closed = true; processEventsExecutor.shutdown(); } private void processEvents() { - LOGGER.info().append("Stream view# processing has started").commit(); + LOGGER.info().append("[").append(tbId).append("] Stream view# processing has started").commit(); while (!closed) { try { processViewsStream( ViewsStreamUtils.getViewsStream(timebaseService.getConnection()) ); } catch (Throwable t) { - LOGGER.error().append("View processing failed: ").append(t.getMessage()).commit(); + LOGGER.error().append("[").append(tbId).append("] View processing failed").append(t).commit(); } initialized = false; @@ -100,14 +95,13 @@ private void processEvents() { threadSleep(10000); } } - LOGGER.info().append("Stream view# processing has completed").commit(); + LOGGER.info().append("[").append(tbId).append("] Stream view# processing has completed").commit(); } private void processViewsStream(DXTickStream viewStream) { this.stream = viewStream; - DBLock lock = stream.lock(); try (TickCursor cursor = openCursor(stream)) { - LOGGER.info().append("Initializing views metadata...").commit(); + LOGGER.info().append("[").append(tbId).append("] Initializing views metadata...").commit(); while (cursor.next()) { if (closed) { return; @@ -118,7 +112,7 @@ private void processViewsStream(DXTickStream viewStream) { if (message instanceof RealTimeStartMessage) { finishInit(); initialized = true; - LOGGER.info().append("Initializing views metadata finished").commit(); + LOGGER.info().append("[").append(tbId).append("] Initializing views metadata finished").commit(); } else if (message instanceof ViewMetadataMessage) { init((ViewMetadataMessage) message); } @@ -128,11 +122,10 @@ private void processViewsStream(DXTickStream viewStream) { } } catch (Throwable t) { if (!closed) { - LOGGER.error().append("View metadata events processor failed").append(t).commit(); + LOGGER.error().append("[").append(tbId).append("] View metadata events processor failed").append(t).commit(); } } finally { stream = null; - lock.release(); } } @@ -148,11 +141,11 @@ private TickCursor openCursor(DXTickStream views) { private TickLoader openLoader() { DXTickStream stream = getViewsMdStream(); if (stream == null) { - throw new RuntimeException("Can't find view md stream"); + throw new RuntimeException("[" + tbId + "] Can't find view md stream"); } TickLoader loader = stream.createLoader(LoadingOptions.withRewriteMode(false)); - loader.addEventListener((e) -> LOGGER.error().append("Failed to send message").append(e).commit()); + loader.addEventListener((e) -> LOGGER.error().append("[").append(tbId).append("] Failed to send message").append(e).commit()); return loader; } @@ -164,7 +157,7 @@ private DXTickStream getViewsMdStream() { @Override public List findAll() { if (!initialized) { - throw new RuntimeException("View md cache is not initialized"); + throw new RuntimeException("[" + tbId + "] View md cache is not initialized"); } return new ArrayList<>(views.values()); @@ -173,7 +166,7 @@ public List findAll() { @Override public ViewMd findById(String id) { if (!initialized) { - throw new RuntimeException("View md cache is not initialized"); + throw new RuntimeException("[" + tbId + "] View md cache is not initialized"); } return views.get(id); @@ -187,7 +180,7 @@ public void saveAll(List streamViews) { @Override public void saveAll(ViewMd... streamViews) { if (!initialized) { - throw new RuntimeException("View md cache is not initialized"); + throw new RuntimeException("[" + tbId + "] View md cache is not initialized"); } try (TickLoader loader = openLoader()) { @@ -204,7 +197,7 @@ public void saveAll(ViewMd... streamViews) { @Override public void delete(ViewMd viewMd) { if (!initialized) { - throw new RuntimeException("View md cache is not initialized"); + throw new RuntimeException("[" + tbId + "] View md cache is not initialized"); } if (viewMd instanceof MutableViewMd) { @@ -248,9 +241,9 @@ private void notifyUpdated(ViewMd viewMd) { } private void init(ViewMetadataMessage viewMdMessage) { - ViewMd viewMd = ViewMdUtils.INSTANCE.fromMessage(viewMdMessage); + ViewMd viewMd = ViewMdUtils.INSTANCE.fromMessage(viewMdMessage, tbId); if (viewMd == null) { - LOGGER.warn().append("Unknown view metadata type: ").append(viewMdMessage).commit(); + LOGGER.warn().append("[").append(tbId).append("] Unknown view metadata type: ").append(viewMdMessage).commit(); } else { init(viewMd); } @@ -269,9 +262,9 @@ private void finishInit() { } private void process(ViewMetadataMessage viewMdMessage) { - ViewMd viewMd = ViewMdUtils.INSTANCE.fromMessage(viewMdMessage); + ViewMd viewMd = ViewMdUtils.INSTANCE.fromMessage(viewMdMessage, tbId); if (viewMd == null) { - LOGGER.warn().append("Unknown view metadata type: ").append(viewMdMessage).commit(); + LOGGER.warn().append("[").append(tbId).append("] Unknown view metadata type: ").append(viewMdMessage).commit(); } else { process(viewMd); } @@ -309,7 +302,7 @@ private void clearRemovedViews() { private void clearViewsMd(String... ids) { try { - LOGGER.info().append("Removed views: ").append( + LOGGER.info().append("[").append(tbId).append("] Removed views: ").append( Arrays.toString(ids) ).commit(); @@ -322,7 +315,7 @@ private void clearViewsMd(String... ids) { ); } } catch (Throwable t) { - LOGGER.error().append("Failed to clear view md from in stream").append(t).commit(); + LOGGER.error().append("[").append(tbId).append("] Failed to clear view md from in stream").append(t).commit(); } } @@ -335,7 +328,7 @@ private void deleteViewStreams(String... keys) { } } } catch (Throwable t) { - LOGGER.error().append("Failed to delete stream for removed view md").append(t).commit(); + LOGGER.error().append("[").append(tbId).append("] Failed to delete stream for removed view md").append(t).commit(); } } @@ -370,7 +363,7 @@ public void clearIfNeed() { new ConstantIdentityKey(viewMd.getId()) ); - LOGGER.info().append("Instruments ").append(stream.getKey()) + LOGGER.info().append("[").append(viewMd.getTbId()).append("] Instruments ").append(stream.getKey()) .append("[").append(viewMd.getId()).append("] cleared with last timestamp ").append(timestamp) .commit(); } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/processor/ViewMdProcessorImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/processor/ViewMdProcessorImpl.java index d307e682..84ed382d 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/processor/ViewMdProcessorImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/services/view/processor/ViewMdProcessorImpl.java @@ -20,6 +20,7 @@ import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.tbwg.messages.ViewState; import com.epam.deltix.tbwg.webapp.services.tasks.workers.FixedSizeWorkersManager; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.view.md.QueryViewMd; import com.epam.deltix.tbwg.webapp.services.view.md.ViewMd; @@ -32,7 +33,7 @@ public class ViewMdProcessorImpl implements ViewMdEventsListener, ViewProcessing private static final Log LOGGER = LogFactory.getLog(ViewMdProcessorImpl.class); - private final TimebaseService timebaseService; + private final TimebaseRegistry registry; private final ViewProcessingListener viewProcessingListener; private final ExecutorService actionsExecutor = Executors.newSingleThreadExecutor(); @@ -40,10 +41,10 @@ public class ViewMdProcessorImpl implements ViewMdEventsListener, ViewProcessing private final Map runningWorkers = new ConcurrentHashMap<>(); - public ViewMdProcessorImpl(TimebaseService timebaseService, + public ViewMdProcessorImpl(TimebaseRegistry registry, ViewProcessingListener viewProcessingListener, FixedSizeWorkersManager workersManager) { - this.timebaseService = timebaseService; + this.registry = registry; this.viewProcessingListener = viewProcessingListener; this.workersManager = workersManager; } @@ -111,6 +112,7 @@ private void startTaskAction(ViewMd viewMd) { } if (viewMd instanceof QueryViewMd) { + TimebaseService timebaseService = registry.resolve(viewMd.getTbId()); QueryViewProcessingWorker worker = new QueryViewProcessingWorker((QueryViewMd) viewMd, this, timebaseService); runningWorkers.put(viewMd.getId(), worker); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/TimebaseSettings.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/TimebaseSettings.java index cedb0fa5..c8de857d 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/TimebaseSettings.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/TimebaseSettings.java @@ -25,6 +25,7 @@ @ConfigurationProperties(prefix = "timebase") public class TimebaseSettings { + private String id; private String url; private String user; private String password; @@ -37,6 +38,14 @@ public class TimebaseSettings { private TbUacSettings uac = new TbUacSettings(); + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + public boolean isOauth2ClientConfigured() { return oauth2Client != null && oauth2Client.getUrl() != null; } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/TimebasesListSettings.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/TimebasesListSettings.java new file mode 100644 index 00000000..67804f69 --- /dev/null +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/settings/TimebasesListSettings.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 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.settings; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * Holds the multi-instance TimeBase configuration bound from the {@code timebases:} YAML list. + * When this list is empty, {@link com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistryImpl} + * falls back to the legacy single-instance {@link TimebaseSettings}. + */ +@Component +@ConfigurationProperties +public class TimebasesListSettings { + + private List timebases = new ArrayList<>(); + + public List getTimebases() { + return timebases; + } + + public void setTimebases(List timebases) { + this.timebases = timebases; + } +} diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/LiveServiceImpl.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/LiveServiceImpl.java index 8adf0e75..dd482522 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/LiveServiceImpl.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/LiveServiceImpl.java @@ -20,7 +20,7 @@ import com.epam.deltix.gflog.api.LogFactory; import com.epam.deltix.tbwg.webapp.config.WebSocketConfig; import com.epam.deltix.tbwg.webapp.services.MetricsService; -import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.util.concurrent.QuickExecutor; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -60,21 +60,21 @@ public class LiveServiceImpl implements WebSocketConfigurer { private final QuickExecutor executor = QuickExecutor.createNewInstance("Live Handler", null); - private final TimebaseService service; + private final TimebaseRegistry tbRegistry; private final MetricsService metrics; - public LiveServiceImpl(TimebaseService timebaseService, MetricsService metrics) { - this.service = timebaseService; + public LiveServiceImpl(TimebaseRegistry tbRegistry, MetricsService metrics) { + this.tbRegistry = tbRegistry; this.metrics = metrics; } @Override public void registerWebSocketHandlers(@NotNull WebSocketHandlerRegistry registry) { - registry.addHandler(new WSHandler(service, executor, metrics), WS_SELECT_STREAM_TEMPLATE) - .addHandler(new WSHandler(service, executor, metrics), "/ws/v0/select") - .addHandler(new WSQueryHandler(service, executor, metrics), "/ws/v0/query") - .addHandler(new WSHandler(service, executor, metrics, service.getFlushPeriodMs()), WS_SELECT_MONITOR_TEMPLATE) + registry.addHandler(new WSHandler(tbRegistry, executor, metrics), WS_SELECT_STREAM_TEMPLATE) + .addHandler(new WSHandler(tbRegistry, executor, metrics), "/ws/v0/select") + .addHandler(new WSQueryHandler(tbRegistry, executor, metrics), "/ws/v0/query") + .addHandler(new WSHandler(tbRegistry, executor, metrics, tbRegistry.getDefault().getFlushPeriodMs()), WS_SELECT_MONITOR_TEMPLATE) .addInterceptors(new TemplateHandshakeInterceptor()) .addInterceptors(new WebSocketConfig.IpInterceptor()) .setAllowedOrigins("*"); 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 4ab31983..c143e125 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 @@ -21,6 +21,7 @@ import com.epam.deltix.tbwg.webapp.config.WebSocketConfig; import com.epam.deltix.tbwg.webapp.model.ws.*; import com.epam.deltix.tbwg.webapp.services.MetricsService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; 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; @@ -68,6 +69,7 @@ public class WSHandler extends TextWebSocketHandler { static final Log LOGGER = LogFactory.getLog(WSHandler.class); static final String PRINCIPAL_ATTRIBUTE_NAME = "principal"; + static final String TB_SERVICE_ATTRIBUTE = "tbService"; protected static final int MAX_BUFFER_SIZE = 16 * 1024; protected static final int LIMIT_BUFFER_SIZE = MAX_BUFFER_SIZE - (MAX_BUFFER_SIZE % 10); @@ -227,6 +229,7 @@ private List allTasks() { private final DirectChannel channel; protected final TimebaseService timebase; + protected final TimebaseRegistry registry; protected final QuickExecutor executor; @@ -239,12 +242,13 @@ private List allTasks() { // Global selector for multiply streams - public WSHandler(TimebaseService timebase, QuickExecutor executor, MetricsService metrics) { - this(timebase, executor, metrics, 0); + public WSHandler(TimebaseRegistry registry, QuickExecutor executor, MetricsService metrics) { + this(registry, executor, metrics, 0); } - public WSHandler(TimebaseService timebase, QuickExecutor executor, MetricsService metrics, long flushPeriodMs) { - this.timebase = timebase; + public WSHandler(TimebaseRegistry registry, QuickExecutor executor, MetricsService metrics, long flushPeriodMs) { + this.registry = registry; + this.timebase = registry.getDefault(); this.executor = executor; this.channel = null; this.gson = createGson(); @@ -253,10 +257,11 @@ public WSHandler(TimebaseService timebase, QuickExecutor executor, MetricsServic this.metrics = metrics; } - public WSHandler(TimebaseService timebase, DirectChannel channel, QuickExecutor executor, MetricsService metrics) { + public WSHandler(TimebaseRegistry registry, DirectChannel channel, QuickExecutor executor, MetricsService metrics) { this.executor = executor; this.channel = channel; - this.timebase = timebase; + this.registry = registry; + this.timebase = registry.getDefault(); this.gson = createGson(); this.flushPeriodMs = 0; this.scheduler = initTaskScheduler(); @@ -287,12 +292,22 @@ protected String endpoint() { return useCache() ? "/ws/v0/monitor" : "/ws/v0/select"; } + protected TimebaseService resolveService(WebSocketSession session) { + MultiValueMap params = + UriComponentsBuilder.fromUriString(session.getUri().toString()).build().getQueryParams(); + List tbParam = params.get("tb"); + String tbId = (tbParam != null && !tbParam.isEmpty()) ? tbParam.get(0) : null; + return registry.resolve(tbId); + } + protected DXTickDB openConnection(WebSocketSession session) { + TimebaseService svc = resolveService(session); + session.getAttributes().put(TB_SERVICE_ATTRIBUTE, svc); Object principal = session.getAttributes().get(PRINCIPAL_ATTRIBUTE_NAME); if (principal instanceof Principal) { TbUserDetails details = TbUserDetails.create(TBWGUtils.getIp(session)); - DXTickDB connection = timebase.login((Principal) principal, details); - timebase.openSession((Principal) principal, details, session.getId()); + DXTickDB connection = svc.login((Principal) principal, details); + svc.openSession((Principal) principal, details, session.getId()); return connection; } else { throw new IllegalStateException("Unknown principal, authentication required."); @@ -300,10 +315,11 @@ protected DXTickDB openConnection(WebSocketSession session) { } protected void closeConnection(WebSocketSession session) { + TimebaseService svc = (TimebaseService) session.getAttributes().getOrDefault(TB_SERVICE_ATTRIBUTE, timebase); Object principal = session.getAttributes().get(PRINCIPAL_ATTRIBUTE_NAME); if (principal instanceof Principal) { TbUserDetails details = TbUserDetails.create(TBWGUtils.getIp(session)); - timebase.closeSession((Principal) principal, details, session.getId()); + svc.closeSession((Principal) principal, details, session.getId()); } } diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSQueryHandler.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSQueryHandler.java index 2cb54402..57ebfc98 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSQueryHandler.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/WSQueryHandler.java @@ -28,6 +28,7 @@ import com.epam.deltix.tbwg.webapp.config.WebSocketConfig; import com.epam.deltix.tbwg.webapp.model.ws.*; import com.epam.deltix.tbwg.webapp.services.MetricsService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.util.concurrent.QuickExecutor; import com.epam.deltix.util.time.TimeKeeper; @@ -41,8 +42,8 @@ public class WSQueryHandler extends WSHandler { - public WSQueryHandler(TimebaseService timebase, QuickExecutor executor, MetricsService metrics) { - super(timebase, executor, metrics); + public WSQueryHandler(TimebaseRegistry registry, QuickExecutor executor, MetricsService metrics) { + super(registry, executor, metrics); } protected String endpoint() { @@ -84,7 +85,7 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message) CharSequence[] instruments = symbols != null ? symbols.toArray(new CharSequence[0]) : null; - InstrumentMessageSource messageSource = timebase.getConnection().executeQuery( + InstrumentMessageSource messageSource = connection.executeQuery( subscribeMessage.query, options, null, instruments, timestamp ); diff --git a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/subscription/SubscriptionService.java b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/subscription/SubscriptionService.java index a77b66af..9c9dae99 100644 --- a/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/subscription/SubscriptionService.java +++ b/java/ws-server/src/main/java/com/epam/deltix/tbwg/webapp/websockets/subscription/SubscriptionService.java @@ -21,6 +21,7 @@ import com.epam.deltix.tbwg.webapp.config.WebSocketConfig; import com.epam.deltix.tbwg.webapp.model.ErrorDef; import com.epam.deltix.tbwg.webapp.services.MetricsService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; 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.HeaderAccessorHelper; @@ -29,6 +30,7 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.AccessDeniedException; import org.springframework.context.annotation.Lazy; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -79,7 +81,7 @@ public SubscriptionInfo(Subscription subscription, String destination) { @Autowired @Lazy - private TimebaseService timebaseService; + private TimebaseRegistry timebaseRegistry; private final MetricsService metrics; @@ -191,10 +193,10 @@ private synchronized void onSubscribe(final SimpMessageHeaderAccessor headers) { Principal principal = headers.getUser(); TbUserDetails details = TbUserDetails.create(TBWGUtils.getIp(headers)); - timebaseService.login(principal, details); + loginAll(principal, details); try { subscription = controller.onSubscribe(headers, channel); - timebaseService.openSession(principal, details, getSessionId(sessionId, subscriptionId)); + openSessionAll(principal, details, getSessionId(sessionId, subscriptionId)); } catch (final Throwable e) { LOG.warn("SubscriptionService controller thew an exception on subscribe: : session=%s, subscription=%s, destination=%s, exception=%s") .with(sessionId) @@ -204,7 +206,7 @@ private synchronized void onSubscribe(final SimpMessageHeaderAccessor headers) { channel.sendError(e); } finally { - timebaseService.logout(principal, details); + logoutAll(principal, details); } if (subscription != null) { @@ -228,7 +230,7 @@ private synchronized void onUnsubscribe(final SimpMessageHeaderAccessor header) .with(subscriptionId) .with(destination); - timebaseService.closeSession(principal, details, getSessionId(sessionId, subscriptionId)); + closeSessionAll(principal, details, getSessionId(sessionId, subscriptionId)); removeSubscription(sessionId, subscriptionId); } @@ -245,7 +247,7 @@ private synchronized void onDisconnect(final SimpMessageHeaderAccessor header) { Objects.requireNonNull(sessionId); List subscriptionIds = removeSubscriptions(sessionId); for (String subscriptionId : subscriptionIds) { - timebaseService.closeSession(principal, details, getSessionId(sessionId, subscriptionId)); + closeSessionAll(principal, details, getSessionId(sessionId, subscriptionId)); } } @@ -349,6 +351,50 @@ private String getSessionId(String sessionId, String subscriptionId) { return sessionId + "|" + subscriptionId; } + private void loginAll(Principal principal, TbUserDetails details) { + for (TimebaseService svc : timebaseRegistry.getAll()) { + try { + svc.login(principal, details); + } catch (AccessDeniedException e) { + throw e; + } catch (Exception e) { + LOG.warn("Failed to login to timebase [%s]: %s").with(svc.getId()).with(e.getMessage()); + } + } + } + + private void openSessionAll(Principal principal, TbUserDetails details, String sessionId) { + for (TimebaseService svc : timebaseRegistry.getAll()) { + try { + svc.openSession(principal, details, sessionId); + } catch (AccessDeniedException e) { + throw e; + } catch (Exception e) { + LOG.warn("Failed to open session on timebase [%s]: %s").with(svc.getId()).with(e.getMessage()); + } + } + } + + private void logoutAll(Principal principal, TbUserDetails details) { + for (TimebaseService svc : timebaseRegistry.getAll()) { + try { + svc.logout(principal, details); + } catch (Exception e) { + LOG.warn("Failed to logout from timebase [%s]: %s").with(svc.getId()).with(e.getMessage()); + } + } + } + + private void closeSessionAll(Principal principal, TbUserDetails details, String sessionId) { + for (TimebaseService svc : timebaseRegistry.getAll()) { + try { + svc.closeSession(principal, details, sessionId); + } catch (Exception e) { + LOG.warn("Failed to close session on timebase [%s]: %s").with(svc.getId()).with(e.getMessage()); + } + } + } + } private static final class SubscriptionChannelImpl implements SubscriptionChannel { diff --git a/java/ws-server/src/main/resources/application.yaml b/java/ws-server/src/main/resources/application.yaml index 8a6edebf..2e29989f 100644 --- a/java/ws-server/src/main/resources/application.yaml +++ b/java/ws-server/src/main/resources/application.yaml @@ -24,6 +24,7 @@ management: web: exposure: include: "metrics,prometheus" +# Single-instance TimeBase configuration: timebase: url: dxtick://localhost:8011 readonly: false @@ -36,6 +37,24 @@ timebase: currencies: tree: group-size: 1000 +# Multi-instance TimeBase configuration: +#timebases: +# - id: tb1 +# url: dxtick://localhost:8011 +# readonly: false +# flushPeriodMs: 500 +# streams: +# exclude: \#$ && \^metrics# +# tree: +# group-size: 1000 +# - id: tb2 +# url: dxtick://localhost:8012 +# readonly: false +# flushPeriodMs: 500 +# streams: +# exclude: \#$ && \^metrics# +# tree: +# group-size: 1000 grafana: pluginsPackages: - com.epam.deltix.grafana diff --git a/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/config/DynamicBarSizeConfig.java b/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/config/DynamicBarSizeConfig.java index e8b8840c..bb29c99a 100644 --- a/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/config/DynamicBarSizeConfig.java +++ b/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/config/DynamicBarSizeConfig.java @@ -17,12 +17,15 @@ package com.epam.deltix.tbwg.webapp.config; import com.epam.deltix.tbwg.webapp.services.charting.datasource.MessageSourceFactory; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import org.mockito.Mockito; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import java.util.List; + @Profile("testCharting") @Configuration public class DynamicBarSizeConfig { @@ -38,4 +41,13 @@ public MessageSourceFactory dataSource() { public TimebaseService timebaseService() { return Mockito.mock(TimebaseService.class); } + + @Bean + public TimebaseRegistry timebaseRegistry(TimebaseService timebaseService) { + return new TimebaseRegistry() { + @Override public List getAll() { return List.of(timebaseService); } + @Override public TimebaseService getById(String id) { return timebaseService; } + @Override public TimebaseService getDefault() { return timebaseService; } + }; + } } \ No newline at end of file diff --git a/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/config/TimebaseInDockerConfig.java b/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/config/TimebaseInDockerConfig.java index aa2e1b5a..7d4a1c0e 100644 --- a/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/config/TimebaseInDockerConfig.java +++ b/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/config/TimebaseInDockerConfig.java @@ -20,6 +20,7 @@ import com.epam.deltix.tbwg.webapp.utils.TimebaseInDocker; import com.epam.deltix.tbwg.webapp.services.charting.datasource.MessageSourceFactory; import com.epam.deltix.tbwg.webapp.services.timebase.SystemMessagesService; +import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseRegistry; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseService; import com.epam.deltix.tbwg.webapp.services.timebase.TimebaseServiceImpl; import com.epam.deltix.tbwg.webapp.settings.TimebaseSettings; @@ -28,6 +29,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import java.util.List; + @Configuration @Profile("withTb") public class TimebaseInDockerConfig { @@ -47,6 +50,15 @@ public TimebaseService timebaseService2(TimebaseSettings timebaseSettings, TbUse return new TimebaseServiceImpl(timebaseSettings, systemMessagesService, userConnectionsService); } + @Bean + public TimebaseRegistry timebaseRegistry(TimebaseService timebaseService) { + return new TimebaseRegistry() { + @Override public List getAll() { return List.of(timebaseService); } + @Override public TimebaseService getById(String id) { return timebaseService; } + @Override public TimebaseService getDefault() { return timebaseService; } + }; + } + @Bean public MessageSourceFactory dataSource() { return Mockito.mock(MessageSourceFactory.class); diff --git a/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/services/ChartingBaseTest.java b/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/services/ChartingBaseTest.java index 08b6dd4f..c846a236 100644 --- a/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/services/ChartingBaseTest.java +++ b/java/ws-server/src/test/java/com/epam/deltix/tbwg/webapp/services/ChartingBaseTest.java @@ -160,8 +160,8 @@ public void setUp(long pointInterval, Instant startTime, Instant endTime, Messag Mockito.when(bookSymbolQuery.getSymbols()).thenReturn(symbols); ReactiveMessageSource reactiveMessageSource = new ReactiveMessageSourceImpl(messageProducer, messageProducer.getObservable()); - Mockito.when(messageSourceFactory.buildSource(any(), any(), anyBoolean(), anyBoolean())).thenReturn(reactiveMessageSource); - Mockito.when(messageSourceFactory.buildSource(any(), any(), anySet(), any(), anyBoolean(), anyBoolean())).thenReturn(reactiveMessageSource); + Mockito.when(messageSourceFactory.buildSource(timebaseService, any(), any(), anyBoolean(), anyBoolean())).thenReturn(reactiveMessageSource); + Mockito.when(messageSourceFactory.buildSource(timebaseService, any(), any(), anySet(), any(), anyBoolean(), anyBoolean())).thenReturn(reactiveMessageSource); } public long runTestFullResponseCheck(long pointInterval, Instant startTime, Instant endTime, String resultFilename, diff --git a/web/frontend/src/app/core/store/app/app.reducer.ts b/web/frontend/src/app/core/store/app/app.reducer.ts index f23a5561..16457947 100644 --- a/web/frontend/src/app/core/store/app/app.reducer.ts +++ b/web/frontend/src/app/core/store/app/app.reducer.ts @@ -33,7 +33,7 @@ export const initialState: State = { name: null, version: null, timestamp: null, - timebase: null, + timebases: [], authentication: null, }, }; diff --git a/web/frontend/src/app/core/styles/_db-selector.scss b/web/frontend/src/app/core/styles/_db-selector.scss new file mode 100644 index 00000000..b8fb1122 --- /dev/null +++ b/web/frontend/src/app/core/styles/_db-selector.scss @@ -0,0 +1,61 @@ +// Requires variables to be imported by the consumer + +.db-indicator { + display: inline-flex; + align-items: center; + margin-left: 8px; + padding: 0 8px; + font-size: 12px; + font-weight: normal; + text-transform: none; + opacity: 0.7; + vertical-align: middle; +} + +.db-selector { + display: inline-block; + width: auto; + min-width: 80px; + max-width: 180px; + height: 28px; + padding: 0 6px; + margin-left: 8px; + font-size: 12px; + font-weight: normal; + text-transform: none; + vertical-align: middle; + color: $nav-color; + background-color: $dark-blue; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 3px; + cursor: pointer; + outline: none; + + option { + background-color: $nav-bg; + color: $white; + } + + &:focus { + border-color: rgba(255, 255, 255, 0.3); + box-shadow: none; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.db-selector-error { + border-color: $red; + color: $red; +} + +.db-selector-hint { + display: block; + font-size: 11px; + font-weight: normal; + text-transform: none; + color: $red; +} diff --git a/web/frontend/src/app/pages/auth-pages/components/login/login.component.ts b/web/frontend/src/app/pages/auth-pages/components/login/login.component.ts index 6a2111f7..918348ca 100644 --- a/web/frontend/src/app/pages/auth-pages/components/login/login.component.ts +++ b/web/frontend/src/app/pages/auth-pages/components/login/login.component.ts @@ -75,7 +75,7 @@ export class LoginComponent implements AfterContentChecked, OnInit, AfterViewIni filter((isLoggedIn) => isLoggedIn), take(1), switchMap(() => this.appInfoService.getAppInfo()), - map(appInfo => appInfo?.timebase?.connected), + map(appInfo => appInfo?.timebases?.some(tb => tb.connected)), takeUntil(this.destroy$), ) .subscribe({ diff --git a/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.html b/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.html index 63d22a74..f2df0cd3 100644 --- a/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.html +++ b/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.html @@ -1,6 +1,25 @@
+ +
+ + +
+ {{ tbId }} +
diff --git a/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.scss b/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.scss index 5f78b6d9..37a92f39 100644 --- a/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.scss +++ b/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.scss @@ -1,4 +1,5 @@ @import 'src/app/core/styles/variables'; +@import 'src/app/core/styles/db-selector'; app-order-book { flex-grow: 1; @@ -30,6 +31,21 @@ ng-multiselect-dropdown { margin-bottom: 15px; } +.db-selector { + height: 34px; + margin-left: 0; + margin-bottom: 15px; + vertical-align: unset; +} + +.db-indicator { + display: inline-block; + vertical-align: unset; + margin-left: 0; + margin-bottom: 15px; + line-height: 34px; +} + .streams-filter { flex-grow: 1; width: 400px; diff --git a/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.ts b/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.ts index 73cdf093..0f0d5421 100644 --- a/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.ts +++ b/web/frontend/src/app/pages/order-book/order-book-page/order-book-page.component.ts @@ -25,15 +25,16 @@ import { SplitterSizesDirective } import { StreamsService } from '../../../shared/services/streams.service'; import { SymbolsService } from '../../../shared/services/symbols.service'; import { TabStorageService } from '../../../shared/services/tab-storage.service'; +import { TimebaseInstanceDef } from '../../../shared/models/timebase-instance-def.model'; import { ChartTypes } from '../../streams/models/chart.model'; import { TabModel } from '../../streams/models/tab.model'; import { StreamUpdatesService } from '../../streams/services/stream-updates.service'; import { UpdateTab } from '../../streams/store/streams-tabs/streams-tabs.actions'; import { getTabsState } from '../../streams/store/streams-tabs/streams-tabs.selectors'; +import { getDefaultTimebase, getTimebases } from '../../streams/store/timebases/timebases.selectors'; import { EOrientations } from '../order-book/order-book.component'; import { StreamSourceService } from '../../streams/services/stream-source.service'; import { StreamModel } from '../../streams/models/stream.model'; -import { ViewsService } from 'src/app/shared/services/views.service'; @Component({ selector: 'app-order-book-page', @@ -43,10 +44,13 @@ import { ViewsService } from 'src/app/shared/services/views.service'; }) export class OrderBookPageComponent implements OnInit, OnDestroy, AfterViewInit { filters: UntypedFormGroup; - streams: { key: string, name: string }[]; - streams$: Observable<{ key: string, name: string }[]>; + streams: { key: string, name: string, tbId?: string }[]; + streams$: Observable<{ key: string, name: string, tbId?: string }[]>; streamNames$: Observable; + streamKeys$: Observable; symbols$: Observable; + tbId: string = null; + timebases$: Observable; loading: boolean = true; noData: boolean = false; hiddenExchanges$: Observable; @@ -54,10 +58,16 @@ export class OrderBookPageComponent implements OnInit, OnDestroy, AfterViewInit orientation$: Observable; orderBookFiltersReady = false; sourceOptions: string[]; - + + get selectedTbUnavailable(): boolean { + return this.timebasesList.find((tb) => tb.id === this.tbId)?.connected === false; + } + private destroy$ = new ReplaySubject(1); private bookState$ = new ReplaySubject(1); private streamsUpdated$ = new BehaviorSubject(null); + private selectedTbId$ = new BehaviorSubject(null); + private timebasesList: TimebaseInstanceDef[] = []; private selectedStreams: string[]; private bookIsEmpty: boolean; @@ -72,14 +82,14 @@ export class OrderBookPageComponent implements OnInit, OnDestroy, AfterViewInit streams: string[]; symbol: string[]; orientation: EOrientations; - source: string + source: string; + timebase: string; }>, private appStore: Store, private activatedRoute: ActivatedRoute, private parentSplitterSizes: SplitterSizesDirective, private streamUpdatesService: StreamUpdatesService, private streamSourceService: StreamSourceService, - private viewService: ViewsService ) {} ngOnInit() { @@ -105,13 +115,32 @@ export class OrderBookPageComponent implements OnInit, OnDestroy, AfterViewInit ...data, streams: data?.streams || (tab.stream ? [tab.stream] : null), symbol: data?.symbol || (tab?.symbol ? [tab.symbol] : null), - source: data?.source ? [data.source] : null - }) - ), + source: data?.source ? [data.source] : null, + timebase: data?.timebase || null, + })), distinctUntilChanged(equal), shareReplay(1), ); + // Initialize timebases list and set default/saved TB + this.timebases$ = this.appStore.pipe(select(getTimebases)); + this.timebases$.pipe(takeUntil(this.destroy$)).subscribe((timebases) => { + this.timebasesList = timebases || []; + }); + storageData$.pipe(take(1)).subscribe((data) => { + if (data?.timebase) { + this.tbId = data.timebase; + this.selectedTbId$.next(data.timebase); + } else { + this.appStore.pipe(select(getDefaultTimebase), take(1)).subscribe((defaultTb) => { + if (defaultTb?.id) { + this.tbId = defaultTb.id; + this.selectedTbId$.next(defaultTb.id); + } + }); + } + }); + this.exchanges$ = storageData$.pipe( map((data) => { if (!data?.symbol?.length) { @@ -171,22 +200,21 @@ export class OrderBookPageComponent implements OnInit, OnDestroy, AfterViewInit streams: current.streams, symbol: current.symbol, source: current.source?.[0], + timebase: this.tbId, hiddenExchanges: freshExchanges ? [] : data?.hiddenExchanges, exchanges: freshExchanges ? null : data?.exchanges, })); }); - this.streams$ = combineLatest( - [this.streamsUpdated$.pipe( - switchMap(() => this.streamsService.getList(true)), - map((streams) => - streams - .filter((s) => !!s.chartType?.find((ct) => ct.chartType === ChartTypes.PRICE_LEVELS)) - .map(({key, name}) => ( { key, name } )) - ), + this.streams$ = combineLatest([this.streamsUpdated$, this.selectedTbId$]).pipe( + switchMap(([, tbId]) => this.streamsService.getList(true, null, null, tbId)), + map((streams) => + streams + .filter((s) => !!s.chartType?.find((ct) => ct.chartType === ChartTypes.PRICE_LEVELS)) + .map(({key, name, tbId}) => ({ key, name, tbId })) ), - this.viewService.getViews().pipe(map(views => views.map(view => ({ key: view.stream, name: view.stream })))) - ]).pipe(map(([streams, viewStreams]) => [...streams, ...viewStreams])); + shareReplay(1), + ); this.filters.get('streams').valueChanges .pipe( @@ -199,13 +227,10 @@ export class OrderBookPageComponent implements OnInit, OnDestroy, AfterViewInit }), delay(500), switchMap((streamList: StreamModel[]) => { - const selectedStreamKeys = []; - streamList.forEach(stream => { - if (this.selectedStreams.includes(stream.name)) { - selectedStreamKeys.push(stream.key); - } - }) - return this.streamSourceService.getAvailableSources(selectedStreamKeys); + const selectedStreamKeys = streamList + .filter(stream => this.selectedStreams.includes(stream.name)) + .map(stream => stream.key); + return this.streamSourceService.getAvailableSources(selectedStreamKeys, this.tbId); }), takeUntil(this.destroy$) ) @@ -222,6 +247,15 @@ export class OrderBookPageComponent implements OnInit, OnDestroy, AfterViewInit this.streamNames$ = this.streams$.pipe(map((s) => s.map(str => str.name))); + this.streamKeys$ = combineLatest([ + this.streams$, + this.filters.get('streams').valueChanges.pipe(startWith(this.filters.get('streams').value)), + ]).pipe( + map(([streamList, selectedNames]) => + streamList.filter(str => (selectedNames as string[]).includes(str.name)).map(str => str.key) + ), + ); + this.symbols$ = this.streams$.pipe( tap(streams => this.streams = streams), switchMap(() => storageData$), @@ -233,7 +267,7 @@ export class OrderBookPageComponent implements OnInit, OnDestroy, AfterViewInit data.streams.map((streamName: string) => { const stream = this.streams.find(streamItem => streamItem.name === streamName); - return this.symbolsService.getSymbols(stream.key).pipe( + return this.symbolsService.getSymbols(stream.key, null, null, stream.tbId).pipe( catchError((e: HttpErrorResponse) => { if (e.status === 400 && e.error.message.startsWith('Unknown stream')) { this.streamsUpdated$.next(); @@ -329,13 +363,6 @@ export class OrderBookPageComponent implements OnInit, OnDestroy, AfterViewInit this.bookState$.next(false); } - get streamKeys() { - const value = this.filters.get('streams').value; - return this.streams$.pipe( - map(s => s.filter(str => value.includes(str.name)).map(str => str.key)) - ) - } - onExchanges(exchanges: string[]) { if (!exchanges.length) { return; @@ -364,7 +391,21 @@ export class OrderBookPageComponent implements OnInit, OnDestroy, AfterViewInit onOrientationChanged(orientation: EOrientations) { this.tabStorageService.updateDataSync((data) => ({...data, orientation})); } - + + onTbChange(tbId: string) { + this.tbId = tbId; + this.selectedTbId$.next(tbId); + this.filters.patchValue({ streams: [], symbol: [] }); + this.tabStorageService.updateDataSync((data) => ({ + ...data, + timebase: tbId, + streams: [], + symbol: [], + exchanges: null, + hiddenExchanges: [], + })); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); diff --git a/web/frontend/src/app/pages/order-book/order-book/order-book.component.ts b/web/frontend/src/app/pages/order-book/order-book/order-book.component.ts index 2cc3ff33..300a7b80 100644 --- a/web/frontend/src/app/pages/order-book/order-book/order-book.component.ts +++ b/web/frontend/src/app/pages/order-book/order-book/order-book.component.ts @@ -109,7 +109,8 @@ export class OrderBookComponent implements AfterViewInit, OnDestroy, OnChanges { @Input() padding = 30; @Input() showLastTime = false; @Input() source: string; - + @Input() tbId: string; + @Output() ready = new EventEmitter(); @Output() readyWithData = new EventEmitter(); @Output() destroy = new EventEmitter(); @@ -140,6 +141,7 @@ export class OrderBookComponent implements AfterViewInit, OnDestroy, OnChanges { streams: string[]; symbol: string; source: string; + tbId?: string; }>(); private reRun$ = new BehaviorSubject(null); private exchanges$ = new BehaviorSubject>(new Set()); @@ -186,21 +188,21 @@ export class OrderBookComponent implements AfterViewInit, OnDestroy, OnChanges { .pipe( map(([changes]) => changes), filter(changes => changes.symbol && changes.streams && changes.hiddenExchanges && changes.source), - filter(changes => this.lastSubscriptionParams !== changes.symbol + changes.streams + changes.hiddenExchanges + changes.source), - tap(changes => this.lastSubscriptionParams = changes.symbol + changes.streams + changes.hiddenExchanges + changes.source), + filter(changes => this.lastSubscriptionParams !== changes.symbol + changes.streams + changes.hiddenExchanges + changes.source + (changes.tbId || '')), + tap(changes => this.lastSubscriptionParams = changes.symbol + changes.streams + changes.hiddenExchanges + changes.source + (changes.tbId || '')), switchMap((data) => this.symbolConfig(data.symbol)), takeUntil(this.destroy$), ) .subscribe(data => this.runBook(data)); - + combineLatest([ changes$.pipe( distinctUntilChanged(equal), filter(changes => changes.symbol && changes.streams && changes.hiddenExchanges && changes.source), - filter(changes => this.lastSubscriptionParams !== changes.symbol + changes.streams + changes.hiddenExchanges + changes.source), + filter(changes => this.lastSubscriptionParams !== changes.symbol + changes.streams + changes.hiddenExchanges + changes.source + (changes.tbId || '')), switchMap(changes => { - this.lastSubscriptionParams = changes.symbol + changes.streams + changes.hiddenExchanges + changes.source; - return this.getFeed(changes.symbol, changes.streams, changes.hiddenExchanges, changes.source); + this.lastSubscriptionParams = changes.symbol + changes.streams + changes.hiddenExchanges + changes.source + (changes.tbId || ''); + return this.getFeed(changes.symbol, changes.streams, changes.hiddenExchanges, changes.source, changes.tbId); }), ), this.changes$.pipe(map(changes => changes.symbol)), @@ -247,9 +249,10 @@ export class OrderBookComponent implements AfterViewInit, OnDestroy, OnChanges { streams: this.streams, hiddenExchanges: this.hiddenExchanges, symbol: this.symbol, - source: this.source + source: this.source, + tbId: this.tbId, }); - + this.exchanges$ .pipe( bufferTime(300), @@ -285,7 +288,8 @@ export class OrderBookComponent implements AfterViewInit, OnDestroy, OnChanges { hiddenExchanges: this.hiddenExchanges, symbol: this.symbol, streams: this.streams, - source: this.source + source: this.source, + tbId: this.tbId, }); } @@ -336,7 +340,7 @@ export class OrderBookComponent implements AfterViewInit, OnDestroy, OnChanges { this.dataReady$.next(false); this.ready$.next(false); const orderGridKernel = new OrderGridEmbeddableKernel(params); - const feed$: Observable = this.getFeed(this.symbol, this.streams, this.hiddenExchanges, this.source) + const feed$: Observable = this.getFeed(this.symbol, this.streams, this.hiddenExchanges, this.source, this.tbId) .pipe( tap((message) => { const data = this.exchanges$.getValue(); @@ -473,20 +477,24 @@ export class OrderBookComponent implements AfterViewInit, OnDestroy, OnChanges { }); } - private getFeed(symbol: string, streams: string[], hiddenExchanges: string[], source: string): Observable { + private getFeed(symbol: string, streams: string[], hiddenExchanges: string[], source: string, tbId?: string): Observable { if (this.feed$) { return this.feed$.pipe( tap(feed => this.precisionFromFeedData(feed)), map(feed => ({...feed, entries: feed.entries.map(entry => ({...entry, quantity: entry.quantity || 0}))})) ); } else { + const headers: { [key: string]: string } = { + instrument: symbol, + streams: JSON.stringify(streams), + hiddenExchanges: JSON.stringify(hiddenExchanges), + source, + }; + if (tbId) { + headers.tbId = tbId; + } return this.wsService - .watchObject('/user/topic/order-book', { - instrument: symbol, - streams: JSON.stringify(streams), - hiddenExchanges: JSON.stringify(hiddenExchanges), - source - }).pipe( + .watchObject('/user/topic/order-book', headers).pipe( tap(feed => { this.precisionFromFeedData(feed); this.noDataForInDepthChart = feed.type === 'snapshot_full_refresh' && feed.entries.length < 3 ? true : false; diff --git a/web/frontend/src/app/pages/query/create-view/create-view-query.component.ts b/web/frontend/src/app/pages/query/create-view/create-view-query.component.ts index 1c2d5e11..fc246aa3 100644 --- a/web/frontend/src/app/pages/query/create-view/create-view-query.component.ts +++ b/web/frontend/src/app/pages/query/create-view/create-view-query.component.ts @@ -18,6 +18,7 @@ import { noSpecialSymbols } from '../../../shared/utils/validators'; export class CreateViewQueryComponent implements OnInit { query: string; + tbId: string; titleControl: UntypedFormControl; liveView: UntypedFormControl; beErrorText: string; @@ -39,7 +40,7 @@ export class CreateViewQueryComponent implements OnInit { } create() { - this.viewsService.save(this.titleControl.value, this.query, this.liveView.value).pipe( + this.viewsService.save(this.titleControl.value, this.query, this.liveView.value, this.tbId).pipe( switchMap(() => this.translateService.get('qqlEditor.createViewModal.successCreated', {name: this.titleControl.value})), tap(message => { this.bsModalRef.hide(); diff --git a/web/frontend/src/app/pages/query/query.component.html b/web/frontend/src/app/pages/query/query.component.html index fbd21f0c..805173d6 100644 --- a/web/frontend/src/app/pages/query/query.component.html +++ b/web/frontend/src/app/pages/query/query.component.html @@ -18,19 +18,19 @@
@@ -72,8 +72,8 @@
@@ -95,10 +95,31 @@
+ + + + {{ tbId }} + + + Selected timebase is unavailable. Choose another one to continue. + +
+ [selectedRange]="selectedRange" [streamId]="streamId" [symbolName]="symbolName" + [scrollRange]="scrollRange" [symbolList]="symbolList$ | async" [tbId]="(currentTab$ | async)?.tbId">
{{ chartDate$ | async }}
diff --git a/web/frontend/src/app/pages/streams/components/deltix-charts/charts/deltix-charts.component.ts b/web/frontend/src/app/pages/streams/components/deltix-charts/charts/deltix-charts.component.ts index 54388a99..06e82f90 100644 --- a/web/frontend/src/app/pages/streams/components/deltix-charts/charts/deltix-charts.component.ts +++ b/web/frontend/src/app/pages/streams/components/deltix-charts/charts/deltix-charts.component.ts @@ -301,7 +301,7 @@ export class DeltixChartsComponent implements OnInit, AfterViewInit, OnDestroy, filter(([tab,]) => tab && !!this.tabId && !!tab.stream && tab.chart), switchMap(([tab, list]) => { const savedSymbolList = this.chartService.getSavedSymbolList(tab.id); - return this.symbolService.getRanges(tab.stream, savedSymbolList ?? list); + return this.symbolService.getRanges(tab.stream, savedSymbolList ?? list, tab.tbId); }), distinctUntilChanged((r1, r2) => JSON.stringify(r1) === JSON.stringify(r2)), takeUntil(this.destroy$)) @@ -993,8 +993,9 @@ export class DeltixChartsComponent implements OnInit, AfterViewInit, OnDestroy, this.symbolName, tab.space, barChartTypes.includes(tab.filter.chart_type) ? tab.filter.period.aggregation : null, + tab.tbId, ), - this.streamsService.rangeCached(tab.stream, this.symbolName, tab.space), + this.streamsService.rangeCached(tab.stream, this.symbolName, tab.space, null, tab.tbId), ]).pipe(map(([range, pureRange]) => [range, pureRange.end, tab])); }), debounceTime(300), @@ -1002,7 +1003,7 @@ export class DeltixChartsComponent implements OnInit, AfterViewInit, OnDestroy, takeUntil(this.retry$), switchMap(([range, pureRangeEnd, tab]) => { const lines$ = tab.filter.chart_type === ChartTypes.LINEAR ? - this.chartsHttpService.linesInfo(tab.stream) : + this.chartsHttpService.linesInfo(tab.stream, tab.tbId) : of([]); return lines$.pipe( @@ -1013,7 +1014,7 @@ export class DeltixChartsComponent implements OnInit, AfterViewInit, OnDestroy, map(([linesAndColors, currentTab]) => [range, pureRangeEnd, currentTab, linesAndColors]), ); }), - switchMap(([range, pureRangeEnd, currentTab, linesAndColors]) => this.streamsService.getProps(this.streamId) + switchMap(([range, pureRangeEnd, currentTab, linesAndColors]) => this.streamsService.getProps(this.streamId, true, (currentTab as TabModel)?.tbId) .pipe(take(1), map(result => result.props.periodicity?.milliseconds), takeUntil(this.destroy$), map(periodicity => [range, pureRangeEnd, currentTab, linesAndColors, periodicity])) ) @@ -1682,4 +1683,4 @@ export class DeltixChartsComponent implements OnInit, AfterViewInit, OnDestroy, }; }, 1000); } -} \ No newline at end of file +} diff --git a/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.html b/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.html index 47c0a612..126c3e77 100644 --- a/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.html +++ b/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.html @@ -52,4 +52,11 @@ placement="bottom"> {{ 'filtersPanel.filter' | translate }} + + {{ tbId }} +
diff --git a/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.scss b/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.scss index 0134a4bd..b48d0536 100644 --- a/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.scss +++ b/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.scss @@ -1,9 +1,17 @@ @import '~src/app/core/styles/variables'; +@import '~src/app/core/styles/db-selector'; .hidden { display: none !important; } +.db-indicator { + display: flex; + color: $nav-color; + white-space: nowrap; + cursor: default; +} + ::ng-deep { deltix-ng-smart-date-time-picker [bsdatepicker] { position: absolute; diff --git a/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.ts b/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.ts index 0b0d52cc..519917c8 100644 --- a/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.ts +++ b/web/frontend/src/app/pages/streams/components/filters-panel/filters-panel.component.ts @@ -40,6 +40,8 @@ import { TopicService } from '../../modules/schema-editor/services/topic.service import { formatHDate } from 'src/app/shared/locale.timezone'; import { GlobalFilters } from 'src/app/shared/models/global-filters'; import * as NotificationsActions from 'src/app/core/modules/notifications/store/notifications.actions'; +import { getTimebases } from '../../store/timebases/timebases.selectors'; +import { TimebaseInstanceDef } from '../../../../shared/models/timebase-instance-def.model'; const now = new HdDate(); @@ -71,6 +73,8 @@ export class FiltersPanelComponent implements OnInit, OnDestroy { bsConfig$: Observable>; showExportBtn$: Observable; filterByEndDate: boolean; + tbId$: Observable; + tbUrl$: Observable; private destroy$ = new Subject(); private now = new Date(); @@ -99,15 +103,18 @@ export class FiltersPanelComponent implements OnInit, OnDestroy { ngOnInit() { this.activatedRoute.params - .pipe(switchMap((tab) => { - if (tab.stream.endsWith('#topic#')) { - this.stream = tab.stream.slice(0, tab.stream.length - 7); - return this.topicService.getTopicSchema(this.stream); - } else { - this.stream = tab.stream; - return this.schemaService.getSchema(tab.stream, null, true); - } - })) + .pipe( + withLatestFrom(this.appStore.pipe(select(getActiveOrFirstTab))), + switchMap(([tab, activeTab]) => { + if (tab.stream.endsWith('#topic#')) { + this.stream = tab.stream.slice(0, tab.stream.length - 7); + return this.topicService.getTopicSchema(this.stream); + } else { + this.stream = tab.stream; + return this.schemaService.getSchema(tab.stream, null, true, activeTab?.tbId); + } + }), + ) .pipe( map((response) => [...response.types]), takeUntil(this.destroy$), @@ -119,13 +126,14 @@ export class FiltersPanelComponent implements OnInit, OnDestroy { .subscribe((schema) => (this.schema = schema)); const range$ = this.activatedRoute.params.pipe( - switchMap((params) => + withLatestFrom(this.appStore.pipe(select(getActiveOrFirstTab))), + switchMap(([params, activeTab]) => params.stream.endsWith('#topic#') ? of(null) : params.symbol ? this.symbolsService - .getProps(params.stream, params.symbol, 1000) + .getProps(params.stream, params.symbol, 1000, true, activeTab?.tbId) .pipe(map((p) => p?.props.symbolRange)) - : this.streamsService.getProps(params.stream, false).pipe(map((p) => p?.props.range)), + : this.streamsService.getProps(params.stream, false, activeTab?.tbId).pipe(map((p) => p?.props.range)), ), shareReplay(1), catchError(e => { @@ -211,7 +219,13 @@ export class FiltersPanelComponent implements OnInit, OnDestroy { this.bsConfig$ = this.globalFiltersService.getBsConfig(true); const activeTab$ = this.appStore.pipe(select(getActiveOrFirstTab)); - + + this.tbId$ = activeTab$.pipe(map((tab) => tab?.tbId || null)); + const timebases$ = this.appStore.pipe(select(getTimebases)); + this.tbUrl$ = combineLatest([this.tbId$, timebases$]).pipe( + map(([tbId, timebases]) => timebases?.find((tb: TimebaseInstanceDef) => tb.id === tbId)?.url || null), + ); + this.showExportBtn$ = activeTab$.pipe( filter((t) => !!t), map((tab) => !tab.monitor && !tab.live), @@ -224,7 +238,7 @@ export class FiltersPanelComponent implements OnInit, OnDestroy { debounceTime(100), switchMap(([updates, activeTab]) => { if (updates.changed.includes(activeTab.stream) && activeTab.filter?.filter_symbols) { - return this.symbolsService.getSymbols(activeTab.stream, activeTab.space); + return this.symbolsService.getSymbols(activeTab.stream, activeTab.space, null, activeTab.tbId); } return of(null); @@ -310,6 +324,7 @@ export class FiltersPanelComponent implements OnInit, OnDestroy { stream: tab.stream, symbol: tab.symbol, space: tab.space, + tbId: tab.tbId, }; const bsModalRef = this.modalService.show(ModalFilterComponent, { @@ -335,7 +350,7 @@ export class FiltersPanelComponent implements OnInit, OnDestroy { .pipe(take(1)) .subscribe((tab) => { const initialState = { - stream: {id: tab.stream, name: tab.stream}, + stream: {id: tab.stream, name: tab.stream, tbId: tab.tbId}, exportFormat: ExportFilterFormat.QSMSG, symbols: tab.symbol ? [tab.symbol] : tab.filter.filter_symbols, types: tab.filter.filter_types, diff --git a/web/frontend/src/app/pages/streams/components/left-sidebar/left-sidebar.component.ts b/web/frontend/src/app/pages/streams/components/left-sidebar/left-sidebar.component.ts index 4cf33ef3..2127558a 100644 --- a/web/frontend/src/app/pages/streams/components/left-sidebar/left-sidebar.component.ts +++ b/web/frontend/src/app/pages/streams/components/left-sidebar/left-sidebar.component.ts @@ -7,6 +7,7 @@ import { distinctUntilChanged, map, take, takeUntil } from 'rxj import { AppState } from '../../../../core/store'; import { getAppInfo } from '../../../../core/store/app/app.selectors'; +import { getActiveOrFirstTab } from '../../store/streams-tabs/streams-tabs.selectors'; import * as StreamsActions from '../../store/streams-list/streams.actions'; import { GlobalResizeService } from '../../../../shared/services/global-resize.service'; import { PlaybackService } from '../../services/playback.service'; @@ -32,6 +33,7 @@ export class LeftSidebarComponent implements OnInit { version$: Observable; activePlaybackIds: number[]; showTopics$: Observable; + private activeTabTbId: string = null; private searchValue = ''; @ViewChild(StreamsListComponent) streamsListComponent: StreamsListComponent; @@ -104,6 +106,13 @@ export class LeftSidebarComponent implements OnInit { map((f) => f?.showTopics), distinctUntilChanged(), ); + + this.appStore.pipe( + select(getActiveOrFirstTab), + map(tab => tab?.tbId || null), + distinctUntilChanged(), + takeUntil(this.destroy$), + ).subscribe(tbId => { this.activeTabTbId = tbId; }); } ngOnDestroy() { @@ -135,6 +144,7 @@ export class LeftSidebarComponent implements OnInit { this.bsModalService.show(CreateStreamModalComponent, { class: 'modal-small', ignoreBackdropClick: true, + initialState: { tbId: this.activeTabTbId }, }); } @@ -142,14 +152,15 @@ export class LeftSidebarComponent implements OnInit { this.bsModalService.show(CreateStreamModalComponent, { class: 'modal-small', ignoreBackdropClick: true, - initialState: { topic: true }, + initialState: { topic: true, tbId: this.activeTabTbId }, }); } - + createView() { this.bsModalService.show(CreateViewModalComponent, { ignoreBackdropClick: true, class: 'modal-xl', + initialState: { tbId: this.activeTabTbId }, }); } @@ -171,6 +182,7 @@ export class LeftSidebarComponent implements OnInit { this.bsModalService.show(ModalImportQSMSGFileComponent, { class: 'modal-xl', ignoreBackdropClick: true, + initialState: { tbId: this.activeTabTbId }, }); this.onCloseContextMenu(); } @@ -179,6 +191,7 @@ export class LeftSidebarComponent implements OnInit { this.bsModalService.show(ModalImportCSVFileComponent, { ignoreBackdropClick: true, class: 'modal-xl', + initialState: { tbId: this.activeTabTbId }, }); this.onCloseContextMenu(); } diff --git a/web/frontend/src/app/pages/streams/components/modals/create-stream-modal/create-stream-modal.component.html b/web/frontend/src/app/pages/streams/components/modals/create-stream-modal/create-stream-modal.component.html index 8c5b3216..9007ecad 100644 --- a/web/frontend/src/app/pages/streams/components/modals/create-stream-modal/create-stream-modal.component.html +++ b/web/frontend/src/app/pages/streams/components/modals/create-stream-modal/create-stream-modal.component.html @@ -1,8 +1,19 @@ + {{ (topic ? 'text.onCreateTopic' : 'text.onCreateStream') | translate }} + + + {{ tbId }} + + Selected timebase is unavailable. + -
+
- - - @@ -263,4 +275,4 @@ -
\ No newline at end of file +
diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-import-QSMSG-file/modal-import-QSMSG-file.component.scss b/web/frontend/src/app/pages/streams/components/modals/modal-import-QSMSG-file/modal-import-QSMSG-file.component.scss index 7ef4d62c..8cdd2c5f 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-import-QSMSG-file/modal-import-QSMSG-file.component.scss +++ b/web/frontend/src/app/pages/streams/components/modals/modal-import-QSMSG-file/modal-import-QSMSG-file.component.scss @@ -1,4 +1,5 @@ @import 'src/app/core/styles/variables'; +@import 'src/app/core/styles/db-selector'; .import-file-content { min-width: 268 * $ss; @@ -315,4 +316,4 @@ app-date-range-picker ::ng-deep .date-range { .error-message-not-empty { margin: 0.1rem 0.8rem; -} \ No newline at end of file +} diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-import-QSMSG-file/modal-import-QSMSG-file.component.ts b/web/frontend/src/app/pages/streams/components/modals/modal-import-QSMSG-file/modal-import-QSMSG-file.component.ts index 59f0a663..4f68a0f1 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-import-QSMSG-file/modal-import-QSMSG-file.component.ts +++ b/web/frontend/src/app/pages/streams/components/modals/modal-import-QSMSG-file/modal-import-QSMSG-file.component.ts @@ -51,6 +51,8 @@ import { SchemaClassTypeModel } from 'src/app/shared/models/schema.class.type.mo import { SeLayoutComponent } from '../../../modules/schema-editor/components/se-layout/se-layout.component'; import { AppState } from 'src/app/core/store'; import { getActiveTab } from '../../../store/streams-tabs/streams-tabs.selectors'; +import { getDefaultTimebase, getTimebases } from '../../../store/timebases/timebases.selectors'; +import { TimebaseInstanceDef } from '../../../../../shared/models/timebase-instance-def.model'; import * as NotificationsActions from '../../../../../core/modules/notifications/store/notifications.actions'; import * as StreamsActions from '../../../store/streams-list/streams.actions'; import { KeyValue } from '@angular/common'; @@ -70,6 +72,14 @@ export class ModalImportQSMSGFileComponent implements OnInit, OnDestroy { @ViewChild(SeLayoutComponent) schemaEditor!: SeLayoutComponent; @ViewChild('dataLossWarning') dataLossWarning: TemplateRef; + tbId: string = null; + timebases$: Observable; + private timebasesList: TimebaseInstanceDef[] = []; + + get selectedTbUnavailable(): boolean { + return this.timebasesList.find((tb) => tb.id === this.tbId)?.connected === false; + } + form: UntypedFormGroup; autocomplete$: Observable; @@ -119,6 +129,7 @@ export class ModalImportQSMSGFileComponent implements OnInit, OnDestroy { private confirmationModal: BsModalRef; private uploadProgress$ = new BehaviorSubject(0); private importProgress$ = new BehaviorSubject(0); + private _tbId$ = new BehaviorSubject(null); private uploadId: number; private cancel$ = new Subject(); private destroy$ = new ReplaySubject(1); @@ -136,6 +147,17 @@ export class ModalImportQSMSGFileComponent implements OnInit, OnDestroy { ) {} ngOnInit(): void { + this.timebases$ = this.appStore.pipe(select(getTimebases)); + this.timebases$.pipe(takeUntil(this.destroy$)).subscribe((timebases) => { + this.timebasesList = timebases || []; + }); + this.appStore.pipe(select(getDefaultTimebase), take(1)).subscribe(defaultTb => { + if (!this.tbId && defaultTb?.id) { + this.tbId = defaultTb.id; + } + }); + this._tbId$.next(this.tbId); + this.progress$ = combineLatest([this.uploadProgress$, this.importProgress$]).pipe( map(([uploadProgress, importProgress]) => Math.floor((uploadProgress + importProgress) / 2)), ); @@ -245,19 +267,15 @@ export class ModalImportQSMSGFileComponent implements OnInit, OnDestroy { .pipe(take(1)) .subscribe((filters) => this.form.get('timezone').patchValue(filters.timezone[0].name)); - this.autocomplete$ = this.streamsStore.pipe(select(streamsListStateSelector)).pipe( - map((state) => { - if (!state.streams) { - return []; - } + this.autocomplete$ = this._tbId$.pipe( + switchMap(tbId => this.streamsService.getList(false, null, null, tbId || null)), + map((streams: StreamModel[]) => { if (this.stream) { - this.displayStreamName = state.streams.find(stream => stream.key === this.stream)?.name; - this.form.patchValue({ stream: this.displayStreamName ?? '' }); + this.displayStreamName = streams.find(s => s.key === this.stream)?.name; + this.form.patchValue({ stream: this.displayStreamName ?? '' }); } - - this.streamList = state.streams; - - return state.streams.map((stream) => stream.name); + this.streamList = streams; + return streams.map(s => s.name); }), publishReplay(1), refCount(), @@ -346,6 +364,11 @@ export class ModalImportQSMSGFileComponent implements OnInit, OnDestroy { .subscribe(() => this.cancelImport()); } + onTbChange(tbId: string) { + this.tbId = tbId; + this._tbId$.next(tbId); + } + onStreamChange(search: string) { this.form.get('stream').patchValue(search); } @@ -501,7 +524,7 @@ export class ModalImportQSMSGFileComponent implements OnInit, OnDestroy { from: formData.setRange ? formData.range.start : null, to: formData.setRange ? formData.range.end : null, writeMode: formData.writeMode, - }); + }, this.tbId); } private catchResponceError(e: HttpErrorResponse) { diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-import-csv-file/modal-import-csv-file.component.html b/web/frontend/src/app/pages/streams/components/modals/modal-import-csv-file/modal-import-csv-file.component.html index 8a811409..9a979be6 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-import-csv-file/modal-import-csv-file.component.html +++ b/web/frontend/src/app/pages/streams/components/modals/modal-import-csv-file/modal-import-csv-file.component.html @@ -2,10 +2,22 @@ [contentClassName]="'import-file-content'" [contentHeightDifference]="170" [modalBodyHeightDifference]="120"> - {{ 'importFromFile.title.importFromTextFile' | translate }} + {{ 'importFromFile.title.importFromTextFile' | translate }} to {{ streamInput ?? currentStreamId }} + + + {{ tbId }} + + Selected timebase is unavailable.
@@ -91,9 +103,9 @@ (click)="previousStep()" class="btn btn-primary"> {{ 'importFromFile.back' | translate }} - - \ No newline at end of file + diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-play-back/modal-play-back.component.scss b/web/frontend/src/app/pages/streams/components/modals/modal-play-back/modal-play-back.component.scss index ade339b7..40dc87ba 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-play-back/modal-play-back.component.scss +++ b/web/frontend/src/app/pages/streams/components/modals/modal-play-back/modal-play-back.component.scss @@ -1,7 +1,14 @@ +@import 'src/app/core/styles/variables'; +@import 'src/app/core/styles/db-selector'; + form { padding: 0rem 2rem; } +.db-selector { + height: 24px; +} + ::ng-deep .modal-xl { min-width: 750px; min-height: 580px; @@ -230,4 +237,4 @@ app-btn-date-picker { .target-streams-label { margin-top: 0.5rem; margin-bottom: 0.2rem; -} \ No newline at end of file +} diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-play-back/modal-play-back.component.ts b/web/frontend/src/app/pages/streams/components/modals/modal-play-back/modal-play-back.component.ts index ff0ae856..b212b0d8 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-play-back/modal-play-back.component.ts +++ b/web/frontend/src/app/pages/streams/components/modals/modal-play-back/modal-play-back.component.ts @@ -1,13 +1,12 @@ import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; import { select, Store } from '@ngrx/store'; -import { map, takeUntil, take, tap, switchMap } from 'rxjs/operators'; +import { map, takeUntil, take, switchMap, filter, distinctUntilChanged } from 'rxjs/operators'; import { Subject, forkJoin, BehaviorSubject, Observable } from 'rxjs'; import { BsModalRef } from 'ngx-bootstrap/modal'; import * as fromStreams from '../../../store/streams-list/streams.reducer'; import * as fromStreamProps from 'src/app/pages/streams/store/stream-props/stream-props.reducer'; -import { streamsListStateSelector } from '../../../store/streams-list/streams.selectors'; import { StreamsService } from 'src/app/shared/services/streams.service'; import { PlaybackService } from '../../../services/playback.service'; import { StreamModel } from '../../../models/stream.model'; @@ -16,6 +15,8 @@ import { camelCaseToWords } from 'src/app/shared/utils/camelCaseToWords'; import { KeyValue } from '@angular/common'; import { GlobalFilterTimeZone } from '../../../models/global.filter.model'; import { GlobalFiltersService } from 'src/app/shared/services/global-filters.service'; +import { getDefaultTimebase, getTimebases } from '../../../store/timebases/timebases.selectors'; +import { TimebaseInstanceDef } from 'src/app/shared/models/timebase-instance-def.model'; @Component({ selector: 'app-modal-play-back', @@ -24,8 +25,21 @@ import { GlobalFiltersService } from 'src/app/shared/services/global-filters.ser }) export class ModalPlayBackComponent implements OnInit { formGroup: FormGroup; - streamNameList: string[] = []; + sourceStreamNameList: string[] = []; + targetStreamNameList: string[] = []; topicNameList: string[] = []; + timebases$: Observable; + sourceTbId: string = null; + targetTbId: string = null; + private timebasesList: TimebaseInstanceDef[] = []; + + get sourceTbUnavailable(): boolean { + return this.timebasesList.find((tb) => tb.id === this.sourceTbId)?.connected === false; + } + + get targetTbUnavailable(): boolean { + return this.timebasesList.find((tb) => tb.id === this.targetTbId)?.connected === false; + } playbackSpeedOptions = ['1', '2', '5', '10', 'MAX']; endTimeMin: Date; validationErrorMessages = { @@ -46,11 +60,14 @@ export class ModalPlayBackComponent implements OnInit { } formInvalid$ = new BehaviorSubject(false); - private allStreamRanges: { streamName: string, startTime: Date, endTime: Date }[]; + private allStreamRanges: { streamName: string, startTime: Date, endTime: Date }[] = []; private currentStreamRanges: { streamName: string, startTime: Date, endTime: Date }[]; - private streamList: StreamModel[]; + private streamList: StreamModel[] = []; + private targetStreamList: StreamModel[] = []; private stream: { id: string, name: string }; private destroy$ = new Subject(); + private _sourceTbId$ = new BehaviorSubject(null); + private _targetTbId$ = new BehaviorSubject(null); public selectedTimezone$: Observable; public targetTypeIsTopic: boolean; @@ -82,33 +99,50 @@ export class ModalPlayBackComponent implements OnInit { endTime: null }); - this.selectedTimezone$ = this.globalFiltersService.getFilters().pipe(map(filters => filters.timezone[0]), takeUntil(this.destroy$)) + this.selectedTimezone$ = this.globalFiltersService.getFilters().pipe(map(filters => filters.timezone[0]), takeUntil(this.destroy$)); - this.streamsStore.pipe(select(streamsListStateSelector)) - .pipe( - tap((state) => this.streamList = state.streams ?? []), - map((state: fromStreams.State) => state.streams.map(stream => stream.name) ?? []), - takeUntil(this.destroy$) - ) - .subscribe(streamNameList => this.streamNameList = streamNameList.filter(name => !!name.trim())); + this.timebases$ = this.streamsStore.pipe(select(getTimebases)); + this.timebases$.pipe(takeUntil(this.destroy$)).subscribe((timebases) => { + this.timebasesList = timebases || []; + }); + + // Load stream lists when source/target TB changes + this._sourceTbId$.pipe( + filter(tbId => tbId != null), + distinctUntilChanged(), + switchMap(tbId => this.streamsService.getList(false, null, null, tbId)), + takeUntil(this.destroy$) + ).subscribe(streams => { + this.streamList = streams; + this.sourceStreamNameList = streams.map(s => s.name).filter(n => !!n.trim()); + this.allStreamRanges = []; + const name = streams.find(s => s.key === this.stream.id)?.name ?? this.stream.name; + this.formGroup.patchValue({ sourceStreams: name ? [name] : [] }); + }); + + this._targetTbId$.pipe( + filter(tbId => tbId != null), + distinctUntilChanged(), + switchMap(tbId => this.streamsService.getList(false, null, null, tbId)), + takeUntil(this.destroy$) + ).subscribe(streams => { + this.targetStreamList = streams; + this.targetStreamNameList = streams.map(s => s.name).filter(n => !!n.trim()); + }); + + // Resolve default TB and trigger initial stream loading + this.streamsStore.pipe(select(getDefaultTimebase), take(1)).subscribe(defaultTb => { + const defaultId = defaultTb?.id ?? null; + if (!this.sourceTbId) { this.sourceTbId = defaultId; } + if (!this.targetTbId) { this.targetTbId = defaultId; } + this._sourceTbId$.next(this.sourceTbId); + this._targetTbId$.next(this.targetTbId); + }); this.topicServics.getTopicList() .pipe(takeUntil(this.destroy$)) .subscribe((topicList: string[]) => this.topicNameList = topicList); - this.streamsService.getProps(this.stream.id) - .pipe( - take(1), - map((response: fromStreamProps.State) => [new Date(response.props.range.start), new Date(response.props.range.end)]), - takeUntil(this.destroy$) - ) - .subscribe(timeRange => { - const [startTime, endTime] = timeRange; - this.formGroup.patchValue({ startTime, endTime }); - this.cdRef.detectChanges(); - this.allStreamRanges = [ { streamName: this.stream.name, startTime, endTime } ]; - }); - this.formGroup.get('startTime').valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(value => this.onTimeChange(value, 'startTime')); @@ -146,8 +180,8 @@ export class ModalPlayBackComponent implements OnInit { if (!this.allStreamRanges.find(item => item.streamName === streamName)) { const streamId = this.streamList.find(stream => stream.name === streamName).key; streamPropsObservables.push( - this.streamsService.getProps(streamId) - .pipe(take(1), + this.streamsService.getProps(streamId, true, this.sourceTbId) + .pipe(take(1), map((response: fromStreamProps.State) => [streamName, response.props.range.start, response.props.range.end]), ) ) @@ -198,6 +232,16 @@ export class ModalPlayBackComponent implements OnInit { }); } + onSourceTbChange(tbId: string) { + this.sourceTbId = tbId; + this._sourceTbId$.next(tbId); + } + + onTargetTbChange(tbId: string) { + this.targetTbId = tbId; + this._targetTbId$.next(tbId); + } + ngOnDestroy() { this.destroy$.next(true); this.destroy$.complete(); @@ -217,8 +261,9 @@ export class ModalPlayBackComponent implements OnInit { const sourceStreams = this.formGroup.value.sourceStreams.map((streamName: string) => { return this.streamList.find(stream => stream.name === streamName).key; }); - const targetStream = isTopic ? this.formGroup.value.targetTopic : - (this.streamList.find(stream => stream.name === this.formGroup.value.targetStream)?.key ?? + const targetLookupList = this.targetStreamList.length ? this.targetStreamList : this.streamList; + const targetStream = isTopic ? this.formGroup.value.targetTopic : + (targetLookupList.find(stream => stream.name === this.formGroup.value.targetStream)?.key ?? this.formGroup.value.targetStream); const params = { ...this.formGroup.value }; @@ -233,7 +278,9 @@ export class ModalPlayBackComponent implements OnInit { speed: speedValue === 'MAX' ? Number.MAX_SAFE_INTEGER : parseFloat(speedValue), from: this.inputsAreDisabled.startTime ? null : this.formGroup.get('startTime').value, to: this.inputsAreDisabled.endTime ? null : this.formGroup.get('endTime').value, - targetTopic: isTopic + targetTopic: isTopic, + sourceTb: this.sourceTbId, + targetTb: this.targetTbId, }).subscribe((id: number) => { this.playbackService.speedValues[id] = speedValue === 'MAX' ? 'MAX' : `${speedValue}x`; this.playbackService.permanenceValues[id] = this.formGroup.get('permanent').value; diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-purge/modal-purge.component.ts b/web/frontend/src/app/pages/streams/components/modals/modal-purge/modal-purge.component.ts index 1ab35c67..1cfc19bf 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-purge/modal-purge.component.ts +++ b/web/frontend/src/app/pages/streams/components/modals/modal-purge/modal-purge.component.ts @@ -35,6 +35,7 @@ export class ModalPurgeComponent implements OnInit, OnDestroy { this.appStore.dispatch( new StreamDetailsActions.GetStreamRange({ streamId: this.stream.id, + tbId: this.stream.tbId, }), ); this.appStore @@ -59,6 +60,7 @@ export class ModalPurgeComponent implements OnInit, OnDestroy { this.appStore.dispatch( new StreamsActions.PurgeStream({ streamKey: this.stream.id, + tbId: this.stream.tbId, params: { timestamp: this.dateValue.getTime(), }, diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-rename/modal-rename.component.ts b/web/frontend/src/app/pages/streams/components/modals/modal-rename/modal-rename.component.ts index ce0c6c3b..233c39bd 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-rename/modal-rename.component.ts +++ b/web/frontend/src/app/pages/streams/components/modals/modal-rename/modal-rename.component.ts @@ -52,7 +52,7 @@ export class ModalRenameComponent implements OnInit, OnDestroy { : this.data.stream.name; const existing$ = this.streamsService - .getList(false) + .getList(false, null, null, this.data.stream.tbId) .pipe(map((streams) => streams .filter(stream => stream.key !== this.data.stream.id) .map((stream) => stream.key))); @@ -99,6 +99,7 @@ export class ModalRenameComponent implements OnInit, OnDestroy { this.appStore.dispatch( new StreamsActions.AskToRenameStream({ streamId: this.data.stream.id, + tbId: this.data.stream.tbId, newName: this.renameForm.get('newName').value, ...(this.data.space ? {spaceName: this.data.space.id} : {}), }), @@ -108,6 +109,7 @@ export class ModalRenameComponent implements OnInit, OnDestroy { this.appStore.dispatch( new StreamsActions.AskToRenameSymbol({ streamId: this.data.stream.id, + tbId: this.data.stream.tbId, ...(this.data.space ? {spaceName: this.data.space.id} : {}), newSymbolName: this.renameForm.get('newName').value, oldSymbolName: this.data.symbol, 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 d98a0a13..e22dcdef 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 @@ -50,7 +50,7 @@ const instrumentTypes = [ styleUrls: ['./modal-send-message.component.scss'], }) export class ModalSendMessageComponent implements OnInit, AfterViewInit, OnDestroy { - stream: {id: string; name: string} | MenuItem; + stream: {id: string; name: string; tbId?: string} | MenuItem; formData: any; editMessageMode: boolean; messageInfo: editedMessageProps; @@ -109,8 +109,8 @@ export class ModalSendMessageComponent implements OnInit, AfterViewInit, OnDestr timestamp: this.formData?.timestamp, }); - this.schema$ = this.schemaService.getSchema(this.stream.id).pipe(shareReplay(1)); - this.symbols$ = this.symbolsService.getSymbols(this.stream.id).pipe(shareReplay(1)); + this.schema$ = this.schemaService.getSchema(this.stream.id, null, false, this.stream.tbId).pipe(shareReplay(1)); + this.symbols$ = this.symbolsService.getSymbols(this.stream.id, null, null, this.stream.tbId).pipe(shareReplay(1)); this.initialCommonValues().subscribe(({symbol, $type, timestamp}) => this.formGroup.patchValue({ @@ -315,7 +315,7 @@ export class ModalSendMessageComponent implements OnInit, AfterViewInit, OnDestr if (!this.symbolEndCache[symbol]) { const nullRange$ = of({props: {symbolRange: {end: '0'}}}); const props$ = symbol ? - this.symbolsService.getProps(this.stream.id, symbol).pipe(catchError(() => nullRange$)) : + this.symbolsService.getProps(this.stream.id, symbol, null, true, this.stream.tbId).pipe(catchError(() => nullRange$)) : nullRange$; this.symbolEndCache[symbol] = props$.pipe( @@ -406,7 +406,7 @@ export class ModalSendMessageComponent implements OnInit, AfterViewInit, OnDestr }); this.streamMessageService - .updateMessage(this.stream.id, JSON.parse(JSON.stringify(form)), this.messageInfo) + .updateMessage(this.stream.id, JSON.parse(JSON.stringify(form)), this.messageInfo, this.stream.tbId) .pipe(withLatestFrom(this.translateService.get('notification_messages'))) .subscribe({ next: ([response, messages]) => { @@ -492,7 +492,7 @@ export class ModalSendMessageComponent implements OnInit, AfterViewInit, OnDestr }); this.streamMessageService - .sendMessage(this.stream.id, [JSON.parse(JSON.stringify(form))], writeMode) + .sendMessage(this.stream.id, [JSON.parse(JSON.stringify(form))], writeMode, this.stream.tbId) .pipe(withLatestFrom(this.translateService.get('notification_messages'))) .subscribe({ next:([response, messages]) => { diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.html b/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.html index 5485ec51..8e56be82 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.html +++ b/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.html @@ -68,9 +68,20 @@
diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.scss b/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.scss index 01330d02..09381c91 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.scss +++ b/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.scss @@ -30,6 +30,30 @@ ng-multiselect-dropdown { margin-top: 10px; } +.container-ch input:disabled ~ .checkmark { + opacity: 0.5; +} + +.container-ch:has(input:disabled) { + opacity: 0.6; +} + +.checkbox-hit-area { + position: relative; + display: inline-block; + width: 18px; + height: 18px; + vertical-align: middle; +} + +.container-ch:has(.checkbox-hit-area) { + padding-left: 0; + + .checkbox-hit-area { + margin-right: 7px; + } +} + .kb-link { position: absolute; left: 1rem; diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.ts b/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.ts index 4680bef9..da5d9b5f 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.ts +++ b/web/frontend/src/app/pages/streams/components/modals/modal-settings/modal-settings.component.ts @@ -52,7 +52,7 @@ export class ModalSettingsComponent implements OnInit, OnDestroy { reverseViewIsDefault: null, showSpaces: null, hideSystemStreams: null, - showTopics: null + showTopics: {value: false, disabled: true} }); this.globalFiltersService.getFilters().pipe(takeUntil(this.destroy$)).subscribe(filters => { @@ -63,7 +63,7 @@ export class ModalSettingsComponent implements OnInit, OnDestroy { reverseViewIsDefault: filters.reverseViewIsDefault, showSpaces: filters.showSpaces, hideSystemStreams: filters.hideSystemStreams, - showTopics: filters.showTopics + showTopics: false }, {emitEvent: false}); this.cdRef.detectChanges(); }); @@ -79,7 +79,7 @@ export class ModalSettingsComponent implements OnInit, OnDestroy { reverseViewIsDefault: data.reverseViewIsDefault, showSpaces: data.showSpaces, hideSystemStreams: data.hideSystemStreams, - showTopics: data.showTopics + showTopics: false }); }); } diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-stream-chart/modal-stream-chart.component.ts b/web/frontend/src/app/pages/streams/components/modals/modal-stream-chart/modal-stream-chart.component.ts index 72f23950..dde96e94 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-stream-chart/modal-stream-chart.component.ts +++ b/web/frontend/src/app/pages/streams/components/modals/modal-stream-chart/modal-stream-chart.component.ts @@ -42,7 +42,7 @@ export class ModalStreamChartComponent implements OnInit, OnDestroy { newTab: localStorage.getItem('chartNewTabLastChoice') || false, }); this.symbols$ = this.symbolsService - .getSymbols(this.item.meta.stream.id) + .getSymbols(this.item.meta.stream.id, null, null, this.item.tbId) .pipe(map((symbols) => symbols.map((s) => ({id: s, name: s})))); this.form @@ -85,6 +85,9 @@ export class ModalStreamChartComponent implements OnInit, OnDestroy { queryParams['stream'] = this.item.meta.stream.id; queryParams['symbol'] = this.form.get('symbol').value.map(item => item.id).join(','); queryParams['name'] = this.item.meta.stream.name; + if (this.item.tbId) { + queryParams['tbId'] = this.item.tbId; + } this.router.navigate( ['/', appRoute, 'chart'], diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-stream-symbols/modal-stream-symbols.component.ts b/web/frontend/src/app/pages/streams/components/modals/modal-stream-symbols/modal-stream-symbols.component.ts index fb191286..9e6d27c2 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-stream-symbols/modal-stream-symbols.component.ts +++ b/web/frontend/src/app/pages/streams/components/modals/modal-stream-symbols/modal-stream-symbols.component.ts @@ -43,7 +43,7 @@ export class ModalStreamSymbolsComponent implements OnInit, OnDestroy { }); this.symbols$ = this.symbolsService - .getSymbols(this.item.meta.stream.id) + .getSymbols(this.item.meta.stream.id, null, null, this.item.tbId) .pipe(map((symbols) => symbols.map((s) => ({id: s, name: s})))); this.form.get('symbol').valueChanges @@ -94,6 +94,7 @@ export class ModalStreamSymbolsComponent implements OnInit, OnDestroy { this.appStore.dispatch( new StreamsActions.AskToDeleteSymbols({ streamKey: this.item.meta.stream.id, + tbId: this.item.tbId, symbols: this.selectedSymbols }), ); @@ -117,4 +118,4 @@ export class ModalStreamSymbolsComponent implements OnInit, OnDestroy { this.destroy$.next(); this.destroy$.complete(); } -} \ No newline at end of file +} diff --git a/web/frontend/src/app/pages/streams/components/modals/modal-truncate/modal-truncate.component.ts b/web/frontend/src/app/pages/streams/components/modals/modal-truncate/modal-truncate.component.ts index d2287ba2..a49d222c 100644 --- a/web/frontend/src/app/pages/streams/components/modals/modal-truncate/modal-truncate.component.ts +++ b/web/frontend/src/app/pages/streams/components/modals/modal-truncate/modal-truncate.component.ts @@ -46,10 +46,10 @@ export class ModalTruncateComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - this.appStore.dispatch(new StreamDetailsActions.GetStreamRange({streamId: this.stream.id})); + this.appStore.dispatch(new StreamDetailsActions.GetStreamRange({streamId: this.stream.id, tbId: this.stream.tbId})); this.symbolsList$ = this.symbolsService - .getSymbols(this.stream.id) + .getSymbols(this.stream.id, null, null, this.stream.tbId) .pipe(map((symbols) => symbols.map((s) => ({id: s, name: s})))); this.appStore @@ -84,6 +84,7 @@ export class ModalTruncateComponent implements OnInit, OnDestroy { this.appStore.dispatch( new StreamsActions.TruncateStream({ streamKey: this.stream.id, + tbId: this.stream.tbId, params: params, }), ); diff --git a/web/frontend/src/app/pages/streams/components/query-btn/query-btn.component.ts b/web/frontend/src/app/pages/streams/components/query-btn/query-btn.component.ts index 0d5ef852..c478e9f3 100644 --- a/web/frontend/src/app/pages/streams/components/query-btn/query-btn.component.ts +++ b/web/frontend/src/app/pages/streams/components/query-btn/query-btn.component.ts @@ -9,6 +9,7 @@ export class QueryBtnComponent implements OnChanges, OnInit { @Input() showText = true; @Input() stream: string; @Input() symbol: string; + @Input() tbId: string; params: {[index: string]: unknown}; @@ -21,6 +22,6 @@ export class QueryBtnComponent implements OnChanges, OnInit { } private freshParams() { - this.params = {newTab: true, querySymbol: this.symbol, queryStream: this.stream}; + this.params = {newTab: true, querySymbol: this.symbol, queryStream: this.stream, ...(this.tbId ? {tbId: this.tbId} : {})}; } } diff --git a/web/frontend/src/app/pages/streams/components/stream-view-reverse/stream-view-reverse.component.ts b/web/frontend/src/app/pages/streams/components/stream-view-reverse/stream-view-reverse.component.ts index b2be84f7..30af57cd 100644 --- a/web/frontend/src/app/pages/streams/components/stream-view-reverse/stream-view-reverse.component.ts +++ b/web/frontend/src/app/pages/streams/components/stream-view-reverse/stream-view-reverse.component.ts @@ -130,6 +130,7 @@ export class StreamViewReverseComponent implements OnInit, OnDestroy { private selectedRowIndex: number; private messageEdited = false; private streamId: string; + private tbId: string; private visibleDefaultColumns: string[]; private gridDefaults: GridOptions = { ...defaultGridOptions, @@ -226,6 +227,7 @@ export class StreamViewReverseComponent implements OnInit, OnDestroy { this.streamId = tab.stream; this.streamName = tab.name; + this.tbId = tab.tbId; this.tabName = tab.stream + tab.id; const sendMessageMenu = { @@ -238,6 +240,7 @@ export class StreamViewReverseComponent implements OnInit, OnDestroy { stream: { id: this.streamId, name: this.streamName, + tbId: this.tbId, }, formData: { ...(event.node.data as StreamDetailsModel)?.original, @@ -264,6 +267,7 @@ export class StreamViewReverseComponent implements OnInit, OnDestroy { stream: { id: this.streamId, name: this.streamName, + tbId: this.tbId, }, formData: { ...(event.node.data as StreamDetailsModel)?.original, @@ -339,8 +343,8 @@ export class StreamViewReverseComponent implements OnInit, OnDestroy { filter(url => !!url.find(segment => segment.path === 'reverse')), switchMap(() => this.activeTab.pipe(filter(tab => !!tab.stream), take(1))), switchMap(tab => (tab.symbol - ? this.symbolsService.getProps(tab.stream, tab.symbol, null, false) - : this.streamsService.getProps(tab.stream, false)).pipe(map(result => ({ props: result.props, currentTab: tab}))) + ? this.symbolsService.getProps(tab.stream, tab.symbol, null, false, tab.tbId) + : this.streamsService.getProps(tab.stream, false, tab.tbId)).pipe(map(result => ({ props: result.props, currentTab: tab}))) ), takeUntil(this.destroy$), ).subscribe(( { props, currentTab } ) => { @@ -545,7 +549,7 @@ export class StreamViewReverseComponent implements OnInit, OnDestroy { switchMap((activeTab: TabModel) => { this.hideGrid$.next(true); this.error$.next(null); - return this.schemaService.getSchema(activeTab.stream, null, true).pipe( + return this.schemaService.getSchema(activeTab.stream, null, true, activeTab.tbId).pipe( map((schema) => [activeTab, schema]), take(1), catchError(e => { @@ -566,7 +570,7 @@ export class StreamViewReverseComponent implements OnInit, OnDestroy { readyEvent.api.setDatasource(this.dataSource.withTab(activeTab, schema.all)); }, ), - switchMap(([activeTab, schema]) => this.streamsService.getProps(activeTab.stream)), + switchMap(([activeTab, schema]) => this.streamsService.getProps(activeTab.stream, true, activeTab.tbId)), tap(streamInfo => { if (streamInfo.props.periodicity.type === 'REGULAR') { this.periodicity = streamInfo.props.periodicity.milliseconds; diff --git a/web/frontend/src/app/pages/streams/components/streams-grid-live/streams-grid-live.component.ts b/web/frontend/src/app/pages/streams/components/streams-grid-live/streams-grid-live.component.ts index 27da2b79..80d13eec 100644 --- a/web/frontend/src/app/pages/streams/components/streams-grid-live/streams-grid-live.component.ts +++ b/web/frontend/src/app/pages/streams/components/streams-grid-live/streams-grid-live.component.ts @@ -68,7 +68,7 @@ export class StreamsGridLiveComponent implements OnInit { filter(p => !!p), ) } else { - return this.schemaService.getSchema(tab.stream, null, true).pipe( + return this.schemaService.getSchema(tab.stream, null, true, tab.tbId).pipe( catchError(e => { this.error$.next(e); return of(null); @@ -88,7 +88,7 @@ export class StreamsGridLiveComponent implements OnInit { if (tab.isTopic) { return of(null); } else { - return this.streamsService.getProps(tab.stream).pipe( + return this.streamsService.getProps(tab.stream, true, tab.tbId).pipe( catchError(e => of(null)), filter(p => !!p), ); @@ -108,11 +108,11 @@ export class StreamsGridLiveComponent implements OnInit { space: tab.space, types: tab.filter.filter_types, }; - + if (tab.symbol) { filters.symbols = [tab.symbol]; } - + if (tab.filter.filter_symbols?.length) { filters.symbols = tab.filter.filter_symbols; } @@ -125,6 +125,10 @@ export class StreamsGridLiveComponent implements OnInit { types: tab.filter.filter_types, } } + + if (tab.tbId) { + filters.tbId = tab.tbId; + } return filters; }), diff --git a/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.html b/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.html index 9f1e2c51..86e8f2e3 100644 --- a/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.html +++ b/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.html @@ -83,19 +83,21 @@ class="sidebar-arrow"> @@ -111,6 +113,14 @@ ({{ menuItem.childrenCount }}) + @@ -133,4 +143,4 @@ M41.1,210.796c0-93.6,75.9-169.5,169.5-169.5s169.6,75.9,169.6,169.5s-75.9,169.5-169.5,169.5S41.1,304.396,41.1,210.796z" /> - \ No newline at end of file + diff --git a/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.scss b/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.scss index ac794aa3..8b765c2a 100644 --- a/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.scss +++ b/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.scss @@ -131,6 +131,22 @@ cdk-virtual-scroll-viewport { pointer-events: none; cursor: default; } + + &[data-type="DB"] { + font-weight: 600; + color: $white; + opacity: 0.9; + } + + &.db-unavailable { + color: $red; + opacity: 0.7; + cursor: default; + + &:hover, &.active { + color: $red; + } + } } .menu-item-title { @@ -145,6 +161,13 @@ cdk-virtual-scroll-viewport { flex-shrink: 0; margin-left: 2 * $ss; } + + .db-unavailable-icon { + flex-shrink: 0; + margin-left: 2 * $ss; + color: $red; + cursor: help; + } } } @@ -280,4 +303,4 @@ i.sidebar-arrow { border-radius: 0.3rem 0.3rem 0rem 0rem; } } -} \ No newline at end of file +} diff --git a/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.ts b/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.ts index 4f8aef2f..be6a20c7 100644 --- a/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.ts +++ b/web/frontend/src/app/pages/streams/components/streams-list/streams-list.component.ts @@ -13,6 +13,7 @@ import { select, Store, } from '@ngrx/store'; +import { Actions, ofType } from '@ngrx/effects'; import { TranslateService } from '@ngx-translate/core'; import equal from 'fast-deep-equal/es6'; import { ContextMenuService } from '@perfectmemory/ngx-contextmenu'; @@ -33,6 +34,7 @@ import { first, map, mapTo, + skip, switchMap, take, takeUntil, @@ -63,6 +65,7 @@ import { StreamsService } from 'src/app/shared/services/streams.service'; import { ImportFromTextFileService } from '../../services/import-from-text-file.service'; import { PlaybackService } from '../../services/playback.service'; import { TabModel } from '../../models/tab.model'; +import { LoadTimebases, TimebasesActionTypes } from '../../store/timebases/timebases.actions'; const defaultTreeViews = [ { @@ -104,6 +107,7 @@ export class StreamsListComponent implements OnInit, OnDestroy { treeViews = defaultTreeViews; streamSelectedForImport: string; selectedItem: string; + activeDbNodeId: string; currentStream: string; currentSymbol: string; public showTopics$: Observable; @@ -111,7 +115,7 @@ export class StreamsListComponent implements OnInit, OnDestroy { public emptyListTitle$: Observable; public searchValue = ''; public searchValueNotFound: boolean; - public highlightedItems: { stream: string, symbol: string }; + public highlightedItems: { stream: string, symbol: string, tbId?: string, streamId?: string }; public synchronizationEnabled: boolean; private scrollSubscription: Subscription; @@ -139,10 +143,13 @@ export class StreamsListComponent implements OnInit, OnDestroy { private hostElement: ElementRef, private streamsService: StreamsService, private importFromTextFileService: ImportFromTextFileService, - private playbackService: PlaybackService + private playbackService: PlaybackService, + private actions$: Actions ) {} ngOnInit() { + this.appStore.dispatch(new LoadTimebases()); + this.isWriter$ = this.permissionsService.isWriter(); this.activeTab$ = this.appStore.pipe(select(getActiveOrFirstTab)); @@ -155,8 +162,8 @@ export class StreamsListComponent implements OnInit, OnDestroy { } this.activeTab$.pipe( - takeUntil(this.destroy$), - distinctUntilChanged((t1, t2) => t1?.stream === t2?.stream && t1?.symbol === t2?.symbol)) + takeUntil(this.destroy$), + distinctUntilChanged((t1, t2) => t1?.stream === t2?.stream && t1?.symbol === t2?.symbol && t1?.tbId === t2?.tbId)) .subscribe(activeTab => { this.activeTabType = activeTab?.type || null; this.highlightedItems = { stream: '', symbol: '' }; @@ -185,7 +192,10 @@ export class StreamsListComponent implements OnInit, OnDestroy { distinctUntilChanged(), ); - showSpaces$.pipe(takeUntil(this.destroy$)).subscribe((showSpaces) => { + showSpaces$.pipe(take(1), takeUntil(this.destroy$)).subscribe(showSpaces => { + this.showSpaces = showSpaces ?? false; + }); + showSpaces$.pipe(skip(1), takeUntil(this.destroy$)).subscribe(showSpaces => { this.toggleSpaces(showSpaces); }); @@ -195,14 +205,13 @@ export class StreamsListComponent implements OnInit, OnDestroy { ); hideSystemStreams$ - .pipe( - takeUntil(this.destroy$), - switchMap(hideSystemStreams => this.freshMenu().pipe(map(() => hideSystemStreams))) - ) + .pipe(takeUntil(this.destroy$)) .subscribe(hideSystemStreams => { this.hideSystemStreams = hideSystemStreams; - this.makeFlatMenu(); - this.cdRef.detectChanges(); + if (this.menuLoaded) { + this.makeFlatMenu(); + this.cdRef.detectChanges(); + } }); this.globalFiltersService.getFilters().pipe( @@ -262,6 +271,23 @@ export class StreamsListComponent implements OnInit, OnDestroy { .onScrollToActiveMenu() .pipe(takeUntil(this.destroy$), delay(0)) .subscribe(() => this.scrollToActiveMenu()); + + // A timebase going down or coming back up can leave the tree stale (e.g. a DB node still + // shown as unavailable after the underlying timebase has recovered, or vice versa) - + // force a full re-fetch and redraw so it reflects the current state. + this.actions$.pipe( + ofType(TimebasesActionTypes.TIMEBASE_STATUS_CHANGED), + takeUntil(this.destroy$), + switchMap(() => { + this.menuItemsService.clearCache(); + this.menuLoaded = false; + this.cdRef.detectChanges(); + return this.freshMenu(false); + }), + ).subscribe(() => { + this.menuLoaded = true; + this.cdRef.detectChanges(); + }); this.leftSidebarStorageService.watchStorage().pipe(map(storage => storage.treeView), distinctUntilChanged()).pipe( switchMap((treeView) => this.structureUpdatesService.onUpdates().pipe(map(updates => ({treeView, updates})))), @@ -273,21 +299,21 @@ export class StreamsListComponent implements OnInit, OnDestroy { ), takeUntil(this.destroy$), ).subscribe((events) => { - const updatedStreams = []; + const updatedStreams: {id: string, tbId?: string}[] = []; events.forEach(event => { if (event.type !== StructureUpdateType.playback) { switch (event.action) { case StructureUpdateAction.rename: - this.onStreamRenamed(event.id, event.target); + this.onStreamRenamed(event.id, event.target, event.tbId); break; case StructureUpdateAction.update: - updatedStreams.push(event.viewMd?.stream || event.id); + updatedStreams.push({id: event.viewMd?.stream || event.id, tbId: event.tbId}); break; case StructureUpdateAction.add: - this.onItemAdded(event.viewMd?.stream || event.id); + this.onItemAdded(event.viewMd?.stream || event.id, event.tbId); break; case StructureUpdateAction.remove: - this.onItemDeleted([event.viewMd?.stream || event.id]); + this.onItemDeleted([event.viewMd?.stream || event.id], event.tbId); break; } } else { @@ -301,11 +327,11 @@ export class StreamsListComponent implements OnInit, OnDestroy { } } }); - + if (updatedStreams.length) { this.onStreamChanged(updatedStreams); } - + this.menuItemsService.clearCache(); }); @@ -456,31 +482,52 @@ export class StreamsListComponent implements OnInit, OnDestroy { } private synchronizeWithTabs(activeTab: TabModel) { - const { stream, symbol } = activeTab; + const { stream, symbol, tbId } = activeTab; const isView = activeTab.isView; const streamName = activeTab.name; + const matchesStream = (menuItem: MenuItem) => + menuItem[isView ? 'name' : 'id'] === (isView ? streamName : stream) && + (!tbId || !menuItem.tbId || menuItem.tbId === tbId); + const symbolList = symbol?.split(','); if (symbol && symbolList.length === 1) { - const streamMenuItemIndex = this.flatMenu?.findIndex(menuItem => menuItem[isView ? 'name' : 'id'] === (isView ? streamName : stream)); + const streamMenuItemIndex = this.flatMenu?.findIndex(matchesStream); const streamMenuItem = this.flatMenu[streamMenuItemIndex]; this.virtualScroll.scrollToIndex(streamMenuItemIndex); this.streamMenuItemIndex = streamMenuItemIndex; - this.openStreamChildrenItems(streamMenuItem, symbol); - this.highlightedItems = { stream: '', symbol }; + this.openStreamChildrenItems(streamMenuItem, symbol, tbId); + this.highlightedItems = { stream: '', symbol, tbId, streamId: stream }; } else if (!symbol) { - const streamMenuItemIndex = this.flatMenu.findIndex(menuItem => menuItem[isView ? 'name' : 'id'] === (isView ? streamName : stream)); + const streamMenuItemIndex = this.flatMenu.findIndex(matchesStream); this.virtualScroll.scrollToIndex(streamMenuItemIndex); } else if (symbol && symbolList.length > 1) { - const streamMenuItemIndex = this.flatMenu.findIndex(menuItem => menuItem[isView ? 'name' : 'id'] === (isView ? streamName : stream)); + const streamMenuItemIndex = this.flatMenu.findIndex(matchesStream); const streamMenuItem = this.flatMenu[streamMenuItemIndex]; this.virtualScroll.scrollToIndex(streamMenuItemIndex); this.streamMenuItemIndex = streamMenuItemIndex; - this.openStreamChildrenItems(streamMenuItem, symbolList[0]); - this.highlightedItems = { stream: streamName, symbol: symbolList[0] }; + this.openStreamChildrenItems(streamMenuItem, symbolList[0], tbId); + this.highlightedItems = { stream: streamName, symbol: symbolList[0], tbId, streamId: stream }; } } + isHighlighted(menuItem: MenuItem): boolean { + if (!this.highlightedItems) return false; + const { stream, symbol, tbId, streamId } = this.highlightedItems; + + if (stream && stream === menuItem.name) { + return !tbId || !menuItem.tbId || menuItem.tbId === tbId; + } + + if (symbol && symbol === menuItem.name) { + if (!streamId) return true; + if (menuItem.parent !== streamId) return false; + return !tbId || !menuItem.path?.length || menuItem.path[0] === tbId; + } + + return false; + } + toggleSynchronization() { this.synchronizationEnabled = !this.synchronizationEnabled; this.structureUpdatesService.saveTabSynchronizationState(this.synchronizationEnabled); @@ -491,7 +538,7 @@ export class StreamsListComponent implements OnInit, OnDestroy { } } - private openStreamChildrenItems(streamMenuItem: MenuItem, symbol: string) { + private openStreamChildrenItems(streamMenuItem: MenuItem, symbol: string, tbId?: string) { return this.leftSidebarStorageService.getStorage() .pipe( switchMap(({treeView, search, searchOptions}) => { @@ -502,6 +549,7 @@ export class StreamsListComponent implements OnInit, OnDestroy { search, treeView === 'views', search ? searchOptions : null, + tbId, ) }), take(1), @@ -640,17 +688,19 @@ export class StreamsListComponent implements OnInit, OnDestroy { })); } - private onStreamRenamed(oldName: string, newName: string) { + private onStreamRenamed(oldName: string, newName: string, tbId?: string) { this.recursiveMenu((item, path) => { - if (item.type === MenuItemType.stream && item.id === oldName) { + if (item.type === MenuItemType.stream && item.id === oldName + && (!tbId || item.tbId === tbId)) { item.id = newName; item.name = newName; - const newPath = `/${encodeURIComponent(newName)}`; + const parentPath = path.substring(0, path.lastIndexOf('/')); + const newPath = `${parentPath}/${encodeURIComponent(newName)}`; this.leftSidebarStorageService.updatePath( (paths) => paths.map((p) => (p.startsWith(path) ? `${newPath}${p.substr(path.length)}` : p)), ) .subscribe(); - + return true; } }); @@ -695,7 +745,48 @@ export class StreamsListComponent implements OnInit, OnDestroy { }); } - private onItemAdded(stream: string) { + private onItemAdded(stream: string, tbId?: string) { + if (tbId) { + const dbNode = this.menu.find(item => item.type === MenuItemType.db && item.id === tbId); + if (!dbNode || dbNode.children?.find(c => c.id === stream)) { + return; + } + + // Collapsed TB node: only bump the count — do not fill children with a partial list. + if (!dbNode.children?.length) { + dbNode.childrenCount = (dbNode.childrenCount ?? 0) + 1; + this.addMeta(); + this.makeFlatMenu(); + this.cdRef.detectChanges(); + return; + } + + const path = `/${encodeURIComponent(tbId)}/${encodeURIComponent(stream)}`; + this.getItems([path], false) + .pipe(take(1)) + .subscribe((rootMenu) => { + const tbNodeInResponse = rootMenu.children?.find(c => c.id === tbId); + const newItem = tbNodeInResponse?.children?.find(c => c.id === stream); + if (newItem) { + dbNode.children = [...dbNode.children, {...newItem, children: [], displayName: newItem.name ?? newItem.id}]; + dbNode.childrenCount = dbNode.children.length; + } + this.addMeta(); + this.makeFlatMenu(); + this.cdRef.detectChanges(); + + const selectedIndex = this.flatMenu.findIndex(elem => elem.id === stream && elem.tbId === tbId); + if (selectedIndex > -1) { + this.virtualScroll.scrollToIndex(selectedIndex); + this.selectedItem = stream; + fromEvent(this.hostElement.nativeElement, 'click') + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe(() => this.resetSelectedItem()); + } + }); + return; + } + if (this.menu.find(item => item.id === stream)) { return; } @@ -703,7 +794,6 @@ export class StreamsListComponent implements OnInit, OnDestroy { .pipe(take(1)) .subscribe((rootMenu) => { const newItem = rootMenu.children.find(menuItem => menuItem.id === stream); - // this.streamsCount = rootMenu.totalCount || rootMenu.childrenCount; if (newItem) { this.menu = [ ...this.menu, {...newItem, children: [], displayName: newItem.name ?? newItem.id } ]; } @@ -722,18 +812,33 @@ export class StreamsListComponent implements OnInit, OnDestroy { }); } - private onItemDeleted(ids: string[]) { - // this.getItems(['/'], false) - // .pipe(take(1)) - // .subscribe((rootMenu) => (this.streamsCount = rootMenu.totalCount || rootMenu.childrenCount)); - this.menu.forEach((item, index) => { - if (ids.includes(item.id)) { - this.leftSidebarStorageService.updatePath((paths) => - paths.filter((path) => path.startsWith(`/${encodeURIComponent(item.id)}`)), - ).subscribe(); - this.menu.splice(index, 1); + private onItemDeleted(ids: string[], tbId?: string) { + if (tbId) { + const dbNode = this.menu.find(item => item.type === MenuItemType.db && item.id === tbId); + if (dbNode?.children?.length) { + ids.forEach(id => { + const idx = dbNode.children.findIndex(c => c.id === id); + if (idx > -1) { + this.leftSidebarStorageService.updatePath((paths) => + paths.filter((path) => path.startsWith(`/${encodeURIComponent(tbId)}/${encodeURIComponent(id)}`)), + ).subscribe(); + dbNode.children.splice(idx, 1); + } + }); + dbNode.childrenCount = dbNode.children.length; + } else if (dbNode) { + dbNode.childrenCount = Math.max(0, (dbNode.childrenCount ?? 0) - ids.length); } - }); + } else { + this.menu.forEach((item, index) => { + if (ids.includes(item.id)) { + this.leftSidebarStorageService.updatePath((paths) => + paths.filter((path) => path.startsWith(`/${encodeURIComponent(item.id)}`)), + ).subscribe(); + this.menu.splice(index, 1); + } + }); + } this.addMeta(); this.makeFlatMenu(); this.cdRef.detectChanges(); @@ -744,46 +849,69 @@ export class StreamsListComponent implements OnInit, OnDestroy { this.cdRef.detectChanges(); } - private onStreamChanged(streams: string[]) { - const indexes = this.menu - ?.map((item, index) => (streams.includes(item.id) ? index : null)) - .filter((i) => i !== null); - if (indexes?.length !== streams.length) { + private onStreamChanged(streams: {id: string, tbId?: string}[]) { + const itemsWithPaths: {item: MenuItem, path: string}[] = []; + + streams.forEach(({id, tbId}) => { + let foundPath: string; + const found = this.recursiveMenu((item, path) => { + if ((item.type === MenuItemType.stream || item.type === MenuItemType.view) + && item.id === id && (!tbId || item.tbId === tbId)) { + foundPath = path; + return true; + } + }); + if (found && foundPath) { + itemsWithPaths.push({item: found, path: foundPath}); + } + }); + + if (itemsWithPaths.length !== streams.length) { timer(100).subscribe(() => this.onStreamChanged(streams)); return; } - - this.freshStreams(indexes); + + this.freshStreamItems(itemsWithPaths); } - - private freshStreams(indexes: number[]) { - const menuItems = indexes.map((index) => ({item: this.menu[index], index})); - const menuItemsPaths = menuItems.map(({item: {id}}) => `/${encodeURIComponent(id)}`); + + private freshStreamItems(itemsWithPaths: {item: MenuItem, path: string}[]) { + const paths = itemsWithPaths.map(({path}) => path); this.leftSidebarStorageService.getStoragePaths().pipe( - map(allPaths => allPaths.filter(path => menuItemsPaths.find(mPath => path.startsWith(mPath)))), - switchMap(requestPaths => this.getItems( - [...menuItemsPaths, ...requestPaths], - false, - )), + map(allPaths => allPaths.filter(p => paths.some(mp => p.startsWith(mp)))), + switchMap(requestPaths => this.getItems([...paths, ...requestPaths], false)), take(1), - ).subscribe((responseItem) => { - menuItems.forEach(({item, index}) => { - const open = item.children?.length > 0; - const itemIndex = responseItem.children?.findIndex((child) => child.id === item.id); - const itemInResponse = responseItem.children[itemIndex]; - if (itemIndex > -1) { + ).subscribe((responseRoot) => { + itemsWithPaths.forEach(({item, path}) => { + const segments = path.split('/').filter(Boolean); + let node: MenuItem = responseRoot; + for (const seg of segments) { + node = node?.children?.find(c => c.id === decodeURIComponent(seg)); + if (!node) { break; } + } + + if (node) { + const open = item.children?.length > 0; if (!open) { - item.childrenCount = itemInResponse.children.length; - item.viewMd = itemInResponse.viewMd; - } else { - this.menu[index] = itemInResponse; + item.childrenCount = node.children?.length ?? node.childrenCount; + item.viewMd = node.viewMd; + } else if (node.type === item.type && node.id === item.id) { + // Only assign when the walked node matches the tree item. + // Partial structure responses must not overwrite a parent DB node with a child STREAM. + const existingChildren = item.children; + Object.assign(item, node); + if (item.type === MenuItemType.db || item.type === MenuItemType.stream || item.type === MenuItemType.view) { + // Keep already-loaded children; response for a deep path may be partial. + if (existingChildren?.length && (!node.children?.length || node.children.length < existingChildren.length)) { + item.children = existingChildren; + } + } } - - this.addMeta(); - this.makeFlatMenu(); - this.cdRef.detectChanges(); } }); + + this.addMeta(); + this.makeFlatMenu(); + this.cdRef.detectChanges(); }); } @@ -793,14 +921,20 @@ export class StreamsListComponent implements OnInit, OnDestroy { path: string = '', ): MenuItem { items = items || this.menu; - return items?.find((item) => { + // Must not use Array.find here: find returns the parent element when the + // predicate returns a nested match object, which breaks callers that expect + // the matched descendant (e.g. freshStreamItems Object.assign'd a STREAM onto a DB node). + for (const item of items || []) { const itemPath = `${path}/${encodeURIComponent(item.id)}`; const inChildren = this.recursiveMenu(callback, item.children || [], itemPath); if (inChildren) { return inChildren; } - return callback(item, itemPath); - }); + if (callback(item, itemPath)) { + return item; + } + } + return undefined; } private addMeta( @@ -810,24 +944,30 @@ export class StreamsListComponent implements OnInit, OnDestroy { symbol = null, chartType = null, isView = false, + tbId: string = null, ) { const targetItems = items || this.menu; targetItems.forEach((item) => { + if (item.type === MenuItemType.db) { + tbId = item.id; + } + if (item.type === MenuItemType.stream || item.type === MenuItemType.view) { stream = item; chartType = item.chartType; } - + isView = isView || item.type === MenuItemType.view; - + if (item.type === MenuItemType.space) { space = item; } - + if (item.type === MenuItemType.identity) { symbol = item.id; } - + + item.tbId = tbId; item.meta = { stream, space, @@ -835,9 +975,9 @@ export class StreamsListComponent implements OnInit, OnDestroy { chartType: chartType || [], isView, }; - - if (item.children?.length) {; - this.addMeta(item.children, stream, space, symbol, chartType, isView); + + if (item.children?.length) { + this.addMeta(item.children, stream, space, symbol, chartType, isView, tbId); } }); } @@ -853,6 +993,12 @@ export class StreamsListComponent implements OnInit, OnDestroy { return JSON.stringify({id: menuItem.id, name: menuItem.name, type: menuItem.type, path: menuItem.path}); } + onDbNodeClick(item: MenuItem, event: MouseEvent): void { + this.activeDbNodeId = item.id; + event.stopPropagation(); + this.cdRef.detectChanges(); + } + public searchMenuItem(searchValue: string) { this.searchValue = searchValue; const selectedIndex = this.flatMenu?.findIndex(elem => elem.nameForSearch.includes(searchValue)); diff --git a/web/frontend/src/app/pages/streams/components/timeline-bar/timeline-bar.component.ts b/web/frontend/src/app/pages/streams/components/timeline-bar/timeline-bar.component.ts index cd38e924..38d4b7ad 100644 --- a/web/frontend/src/app/pages/streams/components/timeline-bar/timeline-bar.component.ts +++ b/web/frontend/src/app/pages/streams/components/timeline-bar/timeline-bar.component.ts @@ -78,9 +78,9 @@ export class TimelineBarComponent implements OnInit, OnDestroy { switchMap((tab) => { return tab.symbol ? this.symbolsService - .getProps(tab.stream, tab.symbol, 1000) + .getProps(tab.stream, tab.symbol, 1000, true, tab.tbId) .pipe(map((p) => p?.props.symbolRange)) - : this.streamsService.getProps(tab.stream).pipe(map((p) => p?.props.range)); + : this.streamsService.getProps(tab.stream, true, tab.tbId).pipe(map((p) => p?.props.range)); }), filter((r) => !!r), distinctUntilChanged(equal), diff --git a/web/frontend/src/app/pages/streams/models/stream.model.ts b/web/frontend/src/app/pages/streams/models/stream.model.ts index 84a0571d..006bc5c1 100644 --- a/web/frontend/src/app/pages/streams/models/stream.model.ts +++ b/web/frontend/src/app/pages/streams/models/stream.model.ts @@ -2,6 +2,7 @@ export interface StreamModel { key: string; name: string; symbols: number; + tbId?: string; chartType?: { chartType: string, title: string }[]; // all custom properties should starts from "_" diff --git a/web/frontend/src/app/pages/streams/models/streams.state.model.ts b/web/frontend/src/app/pages/streams/models/streams.state.model.ts index 56c66925..49d9d5a3 100644 --- a/web/frontend/src/app/pages/streams/models/streams.state.model.ts +++ b/web/frontend/src/app/pages/streams/models/streams.state.model.ts @@ -1,6 +1,7 @@ export interface StreamsStateModel { messageType?: string; id?: number; + tbId?: string; added?: string[]; deleted?: Array; changed?: string[]; diff --git a/web/frontend/src/app/pages/streams/models/structure-update.ts b/web/frontend/src/app/pages/streams/models/structure-update.ts index 26939ce9..370b04b4 100644 --- a/web/frontend/src/app/pages/streams/models/structure-update.ts +++ b/web/frontend/src/app/pages/streams/models/structure-update.ts @@ -3,6 +3,7 @@ export interface StructureUpdate { action: StructureUpdateAction; id: string; target: string; + tbId?: string; viewMd?: { stream: string; }; diff --git a/web/frontend/src/app/pages/streams/models/tab.model.ts b/web/frontend/src/app/pages/streams/models/tab.model.ts index b7566fcf..ce876eaf 100644 --- a/web/frontend/src/app/pages/streams/models/tab.model.ts +++ b/web/frontend/src/app/pages/streams/models/tab.model.ts @@ -10,6 +10,7 @@ export class TabModel { public space?: string; public id?: string; public source?: string; + public tbId?: string; public name?: string; @@ -158,12 +159,18 @@ export class TabModel { if (obj['exchange']) { this.exchange = obj['exchange']; } + if (obj['tbId']) { + this.tbId = obj['tbId']; + } } public get title(): string { let title = this.orderBook ? '' : (this.streamName || this.name || this.stream); if (this.space !== undefined && !this.orderBook) title += ' / ' + (this.space || 'root'); if (this.symbol && !this.orderBook) title += ' / ' + this.symbol; + if (this.tbId && (this.streamCreate || this.topicCreate)) { + title += ` [${this.tbId}]`; + } return title; } @@ -204,6 +211,7 @@ export class TabModel { public get linkQuery(): {[key: string]: string} { const QUERY = {}; if (this.space !== undefined) QUERY['space'] = this.space; + if (this.tbId) QUERY['tbId'] = this.tbId; return QUERY; } diff --git a/web/frontend/src/app/pages/streams/models/ws-live.model.ts b/web/frontend/src/app/pages/streams/models/ws-live.model.ts index 95edf122..2cba5d1e 100644 --- a/web/frontend/src/app/pages/streams/models/ws-live.model.ts +++ b/web/frontend/src/app/pages/streams/models/ws-live.model.ts @@ -5,6 +5,7 @@ export class WSLiveModel { space?: string; types?: string[]; qql?: string; + tbId?: string; constructor(obj: WSLiveModel | {}) { Object.assign(this, obj); diff --git a/web/frontend/src/app/pages/streams/modules/monitor-log/components/monitor-log-grid/monitor-log-grid.component.ts b/web/frontend/src/app/pages/streams/modules/monitor-log/components/monitor-log-grid/monitor-log-grid.component.ts index 2d19ecec..097f8e37 100644 --- a/web/frontend/src/app/pages/streams/modules/monitor-log/components/monitor-log-grid/monitor-log-grid.component.ts +++ b/web/frontend/src/app/pages/streams/modules/monitor-log/components/monitor-log-grid/monitor-log-grid.component.ts @@ -246,7 +246,7 @@ export class MonitorLogGridComponent implements OnInit, OnDestroy { if (tab.stream.endsWith('#topic#')) { return this.topicService.getTopicSchema(tab.stream.slice(0, tab.stream.length - 7)); } else { - return this.schemaService.getSchema(tab.stream, null, true).pipe( + return this.schemaService.getSchema(tab.stream, null, true, tab.tbId).pipe( catchError(e => { this.error$.next(e); return of(null); @@ -397,7 +397,11 @@ export class MonitorLogGridComponent implements OnInit, OnDestroy { if (tab.filter.filter_types && tab.filter.filter_types.length) { socketData.types = tab.filter.filter_types; } - + + if (tab.tbId) { + socketData.tbId = tab.tbId; + } + this.monitorLogGridDataService?.destroy(); this.subIsInited = true; diff --git a/web/frontend/src/app/pages/streams/modules/monitor-log/services/monitor-log-grid-data.service.ts b/web/frontend/src/app/pages/streams/modules/monitor-log/services/monitor-log-grid-data.service.ts index 9f997b92..87dc8746 100644 --- a/web/frontend/src/app/pages/streams/modules/monitor-log/services/monitor-log-grid-data.service.ts +++ b/web/frontend/src/app/pages/streams/modules/monitor-log/services/monitor-log-grid-data.service.ts @@ -98,7 +98,11 @@ export class MonitorLogGridDataService { if (typeof activeTab.space === 'string') { params['space'] = encodeURIComponent(activeTab.space); } - return this.httpClient.post(httpUrl, params); + return this.httpClient.post( + httpUrl, + params, + activeTab.tbId ? {params: {tb: activeTab.tbId}} : {}, + ); }), map((data) => { this.rowsStore = data diff --git a/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.html b/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.html index afe5bc9e..f868f725 100644 --- a/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.html +++ b/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.html @@ -99,7 +99,10 @@ - \ No newline at end of file + diff --git a/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.scss b/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.scss index e299e468..62e18840 100644 --- a/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.scss +++ b/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.scss @@ -1,4 +1,5 @@ @import '~src/app/pages/streams/components/stream-details/stream-details.component'; +@import '~src/app/core/styles/db-selector'; .diff-view { position: relative; @@ -227,4 +228,4 @@ .unused-enums-warning { color: #ffc107; -} \ No newline at end of file +} diff --git a/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.ts b/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.ts index a8227a33..d5565bbe 100644 --- a/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.ts +++ b/web/frontend/src/app/pages/streams/modules/schema-editor/components/se-layout/se-layout.component.ts @@ -307,12 +307,13 @@ export class SeLayoutComponent implements OnInit, OnDestroy { this.newItemModalRef?.hide(); const streamKey = this.insideModal ? this.stream : this.streamName; this.appStore.dispatch(CreateStream({ - key: streamKey, - topic: newTopic, + key: streamKey, + topic: newTopic, version: newTopic ? this.topicService.dataForCopyToStream?.storageVersion : this.streamsService.streamCreationData.storageVersion, distributionFactor: newTopic ? this.topicService.dataForCopyToStream?.distributionFactor : this.streamsService.streamCreationData.distributionFactor, - copyToStream: this.topicStreamKey, - noNotification: this.insideModal })); + copyToStream: this.topicStreamKey, + noNotification: this.insideModal, + tbId: newTopic ? null : (this.streamsService.streamCreationData?.tbId || this.currentTab?.tbId) })); this.topicService.dataForCopyToStream = null; } diff --git a/web/frontend/src/app/pages/streams/modules/schema-editor/store/schema-editor.actions.ts b/web/frontend/src/app/pages/streams/modules/schema-editor/store/schema-editor.actions.ts index fc8c93aa..f9dcd6fb 100644 --- a/web/frontend/src/app/pages/streams/modules/schema-editor/store/schema-editor.actions.ts +++ b/web/frontend/src/app/pages/streams/modules/schema-editor/store/schema-editor.actions.ts @@ -82,7 +82,7 @@ export const RemoveSchemaDiff = createAction( export const CreateStream = createAction( SchemaEditorActionTypes.CREATE_STREAM, - props<{key: string, topic: boolean, copyToStream?: string, version: string, distributionFactor: string, noNotification?: boolean}>(), + props<{key: string, topic: boolean, copyToStream?: string, version: string, distributionFactor: string, noNotification?: boolean, tbId?: string}>(), ); export const SetStreamId = createAction( @@ -183,4 +183,4 @@ export const EditSchemaMergeState = createAction( export const UpdateSchemaAndRemoveType = createAction( SchemaEditorActionTypes.UPDATE_SCHEMA_VALIDITY, props<{ insideModal: boolean, deletingItems?: ClassEnumListItem[] }>(), -); \ No newline at end of file +); diff --git a/web/frontend/src/app/pages/streams/modules/schema-editor/store/schema-editor.effects.ts b/web/frontend/src/app/pages/streams/modules/schema-editor/store/schema-editor.effects.ts index 219f2b0b..0bb0e8bc 100644 --- a/web/frontend/src/app/pages/streams/modules/schema-editor/store/schema-editor.effects.ts +++ b/web/frontend/src/app/pages/streams/modules/schema-editor/store/schema-editor.effects.ts @@ -11,7 +11,8 @@ import { SchemaClassTypeModel, } from '../../../../../shared/models/schema.class.type.model'; import * as StreamsTabsActions from '../../../store/streams-tabs/streams-tabs.actions'; -import {getActiveTab} from '../../../store/streams-tabs/streams-tabs.selectors'; +import {getActiveOrFirstTab, getActiveTab} from '../../../store/streams-tabs/streams-tabs.selectors'; +import {getStreamsList} from '../../../store/streams-list/streams.selectors'; import {StreamMetaDataChangeModel} from '../models/stream.meta.data.change.model'; import { CreateStream, @@ -63,17 +64,23 @@ export class SchemaEditorEffects { map(streamId => ({ streamId, topic })) ), ), - switchMap(({ streamId, topic }) => { + withLatestFrom( + this.appStore.pipe(select(getStreamsList)), + this.appStore.pipe(select(getActiveOrFirstTab)), + ), + switchMap(([{ streamId, topic }, streams, activeTab]) => { + const tbId = streams?.find(s => s.key === streamId)?.tbId ?? activeTab?.tbId; if (!topic) { return this.httpClient$.get<{types: SchemaClassTypeModel[]; all: SchemaClassTypeModel[]}>( `${encodeURIComponent(streamId)}/schema`, { params: { tree: 'true', + ...(tbId ? {tb: tbId} : {}), }, }, ) - } else { + } else { return this.topicService.getTopicSchema(streamId); } }), @@ -90,7 +97,8 @@ export class SchemaEditorEffects { take(1), ), ), - switchMap(([state, streamId]: [State, string]) => { + withLatestFrom(this.appStore.pipe(select(getStreamsList))), + switchMap(([[state, streamId], streams]: [[State, string], any]) => { const all = JSON.parse(JSON.stringify([...state.classes, ...state.enums])), types = all.filter((_type) => _type._props && _type._props._isUsed), schemaMapping = {...state.schemaMapping}; @@ -113,6 +121,7 @@ export class SchemaEditorEffects { }); } }); + const tbId = streams?.find(s => s.key === streamId)?.tbId; return this.httpClient$.post( `/${encodeURIComponent(streamId)}/getSchemaChanges`, { @@ -122,6 +131,7 @@ export class SchemaEditorEffects { }, schemaMapping, }, + {params: tbId ? {tb: tbId} : {}}, ); }), map((diff) => SetSchemaDiff({diff})), @@ -131,7 +141,7 @@ export class SchemaEditorEffects { this.actions$.pipe( ofType(CreateStream), withLatestFrom(this.appStore.pipe(select(getEditSchemaState))), - switchMap(([{key, topic, copyToStream, version, distributionFactor, noNotification}, state]) => { + switchMap(([{key, topic, copyToStream, version, distributionFactor, noNotification, tbId}, state]) => { const all = JSON.parse(JSON.stringify([...state.classes, ...state.enums])), types = all.filter((_type) => _type._props && _type._props._isUsed); all.forEach((_type) => { @@ -172,10 +182,11 @@ export class SchemaEditorEffects { } return this.httpClient$.post( '/topics', params).pipe(map(() => ({ topic, noNotification }))); } else { - const params = { + const params: any = { key, version, - distributionFactor + distributionFactor, + ...(tbId ? {tb: tbId} : {}), }; if (!params.distributionFactor) { delete params.distributionFactor; @@ -220,9 +231,12 @@ export class SchemaEditorEffects { this.actions$.pipe( ofType(SaveSchemaChanges), tap(() => getSaveSchemaData.release()), - withLatestFrom(this.appStore.pipe(select(getSaveSchemaData))), + withLatestFrom( + this.appStore.pipe(select(getSaveSchemaData)), + this.appStore.pipe(select(getStreamsList)), + ), switchMap( - ([action, {schemaMapping, classes, enums, defaultValues, streamId, dropValues}]) => { + ([action, {schemaMapping, classes, enums, defaultValues, streamId, dropValues}, streams]) => { const all = JSON.parse(JSON.stringify([...classes, ...enums])), types = all.filter((_type) => _type._props && _type._props._isUsed); all.forEach((_type) => { @@ -244,6 +258,7 @@ export class SchemaEditorEffects { } }); + const tbId = streams?.find(s => s.key === streamId)?.tbId; return this.httpClient$ .post(`/${encodeURIComponent(streamId)}/changeSchema`, { schemaMapping, @@ -254,6 +269,8 @@ export class SchemaEditorEffects { types, }, background: action.background, + }, { + params: tbId ? {tb: tbId} : {}, }) .pipe( tap(() => { @@ -341,4 +358,4 @@ export class SchemaEditorEffects { private schemaEditorService: SchemaEditorService, private schemaValidityService: SchemaValidityService ) {} -} \ No newline at end of file +} diff --git a/web/frontend/src/app/pages/streams/services/import-from-text-file.service.ts b/web/frontend/src/app/pages/streams/services/import-from-text-file.service.ts index f685883b..ec77691a 100644 --- a/web/frontend/src/app/pages/streams/services/import-from-text-file.service.ts +++ b/web/frontend/src/app/pages/streams/services/import-from-text-file.service.ts @@ -43,6 +43,7 @@ export class ImportFromTextFileService { public sessionId: string; public streamId: string; + public tbId: string = null; public streamIdSubject = new Subject(); public uploadingId: string = ''; public validation = {}; @@ -118,7 +119,8 @@ export class ImportFromTextFileService { public initImport(streamId = this.streamId): Observable { const formData = new FormData(); formData.append('streamKey', streamId); - return this.http.post(this.uploadFileUrl, formData); + const params = this.tbId ? {tb: this.tbId} : {}; + return this.http.post(this.uploadFileUrl, formData, {params}); } public getPreviews() { @@ -230,7 +232,7 @@ export class ImportFromTextFileService { } public getStreamSchema() { - return this.http.get(`${this.streamId}/schema`); + return this.http.get(`${this.streamId}/schema`, this.tbId ? {params: {tb: this.tbId}} : {}); } public validateMappingGeneral() { @@ -283,6 +285,7 @@ export class ImportFromTextFileService { this.uploadedFiles.length = 0; this.sessionId = null; this.streamId = null; + this.tbId = null; this.streamIdSubject.next(this.streamId); this.validation = {}; this.validationSubject.next(this.validation); @@ -472,7 +475,7 @@ export class ImportFromTextFileService { } public createStream(streamName: string, storageVersion: string, distributionFactor: number) { - const params = { + const params: any = { key: streamName, version: storageVersion, distributionFactor @@ -480,6 +483,9 @@ export class ImportFromTextFileService { if (!params.distributionFactor) { delete params.distributionFactor; } + if (this.tbId) { + params.tb = this.tbId; + } const { types, all } = this.createdStreamSchema; return this.http.post('/createStream', { @@ -499,4 +505,4 @@ export class ImportFromTextFileService { public deleteStream(streamKey: string) { return this.http.post(`${encodeURIComponent(streamKey)}/delete`, {}); } -} \ No newline at end of file +} diff --git a/web/frontend/src/app/pages/streams/services/playback.service.ts b/web/frontend/src/app/pages/streams/services/playback.service.ts index 4f63d9c5..3133f678 100644 --- a/web/frontend/src/app/pages/streams/services/playback.service.ts +++ b/web/frontend/src/app/pages/streams/services/playback.service.ts @@ -13,7 +13,9 @@ interface playbackSettings { permanent: boolean; from?: string, to?: string, - targetTopic?: boolean + targetTopic?: boolean, + sourceTb?: string, + targetTb?: string, } interface playBackInfo { @@ -210,4 +212,4 @@ export class PlaybackService { playingPlaybacks: [playback.id], lastActivatedPlaybackId: null }); } }; -} \ No newline at end of file +} diff --git a/web/frontend/src/app/pages/streams/services/stream-data.service.ts b/web/frontend/src/app/pages/streams/services/stream-data.service.ts index bc23c0e4..358f4ed2 100644 --- a/web/frontend/src/app/pages/streams/services/stream-data.service.ts +++ b/web/frontend/src/app/pages/streams/services/stream-data.service.ts @@ -127,6 +127,7 @@ export class StreamDataService implements IDatasource { space: encodeURIComponent(activeTab.space), } : {}), + ...(activeTab.tbId ? {tb: activeTab.tbId} : {}), }, }) .pipe(takeUntil(this.loadedData$.pipe(skip(1)))) diff --git a/web/frontend/src/app/pages/streams/services/stream-source.service.ts b/web/frontend/src/app/pages/streams/services/stream-source.service.ts index 73aebab5..5122d723 100644 --- a/web/frontend/src/app/pages/streams/services/stream-source.service.ts +++ b/web/frontend/src/app/pages/streams/services/stream-source.service.ts @@ -11,11 +11,14 @@ export class StreamSourceService { constructor(private httpClient: HttpClient) {} - getAvailableSources(streams: string[]) { + getAvailableSources(streams: string[], tbId: string = null) { const encodedStreams = streams.map(stream => encodeURIComponent(stream)); - const params = { streams: encodedStreams }; + const params: { [key: string]: string | string[] } = { streams: encodedStreams }; + if (tbId) { + params.tb = tbId; + } - const cashKey = JSON.stringify(streams); + const cashKey = JSON.stringify({ streams, tbId }); if (this.requestsInProgress[cashKey]) { return this.requestsInProgress[cashKey]; } else { diff --git a/web/frontend/src/app/pages/streams/services/structure-updates.service.ts b/web/frontend/src/app/pages/streams/services/structure-updates.service.ts index 900f7345..247808cf 100644 --- a/web/frontend/src/app/pages/streams/services/structure-updates.service.ts +++ b/web/frontend/src/app/pages/streams/services/structure-updates.service.ts @@ -19,14 +19,17 @@ export class StructureUpdatesService { return this.wsService.watchObject(`/topic/streams`); } - getBackgroundTask(streamId: string): Observable { + getBackgroundTask(streamId: string, tbId: string = null): Observable { return this.httpClient.get(`/${encodeURIComponent(streamId)}/options/backgroundTask`, { - headers: { customError: 'true' } + headers: { customError: 'true' }, + params: tbId ? {tb: tbId} : {}, }); } - abortBackgroundTask(streamId: string): Observable { - return this.httpClient.get(`/${encodeURIComponent(streamId)}/abortBackgroundTask`); + abortBackgroundTask(streamId: string, tbId: string = null): Observable { + return this.httpClient.get(`/${encodeURIComponent(streamId)}/abortBackgroundTask`, { + params: tbId ? {tb: tbId} : {}, + }); } saveTabSynchronizationState(enabled: boolean) { diff --git a/web/frontend/src/app/pages/streams/sidebar-context-menu/menu-item-context-menu/menu-item-context-menu.component.ts b/web/frontend/src/app/pages/streams/sidebar-context-menu/menu-item-context-menu/menu-item-context-menu.component.ts index 44543c7f..d38ab1f6 100644 --- a/web/frontend/src/app/pages/streams/sidebar-context-menu/menu-item-context-menu/menu-item-context-menu.component.ts +++ b/web/frontend/src/app/pages/streams/sidebar-context-menu/menu-item-context-menu/menu-item-context-menu.component.ts @@ -1,6 +1,6 @@ import {ChangeDetectorRef, Component, HostListener, Input, OnDestroy, OnInit} from '@angular/core'; import {distinctUntilChanged, map, take, takeUntil} from 'rxjs/operators'; -import {MenuItem} from '../../../../shared/models/menu-item'; +import {MenuItem, MenuItemType} from '../../../../shared/models/menu-item'; import {StreamsNavigationService} from '../../streams-navigation/streams-navigation.service'; import {SidebarContextMenuService} from '../sidebar-context-menu.service'; import { GlobalFiltersService } from 'src/app/shared/services/global-filters.service'; @@ -27,7 +27,8 @@ export class MenuItemContextMenuComponent implements OnInit, OnDestroy { ) {} @HostListener('contextmenu', ['$event']) onRightClick(event) { - if (this.streamsNavigationService.url(this.item, this.activeTabType, this.reverseViewIsDefault$.getValue()) === null) { + const isDb = this.item?.type === MenuItemType.db; + if (!isDb && this.streamsNavigationService.url(this.item, this.activeTabType, this.reverseViewIsDefault$.getValue()) === null) { return; } diff --git a/web/frontend/src/app/pages/streams/sidebar-context-menu/sidebar-context-menu/sidebar-context-menu.component.html b/web/frontend/src/app/pages/streams/sidebar-context-menu/sidebar-context-menu/sidebar-context-menu.component.html index df2a1fac..c6e35639 100644 --- a/web/frontend/src/app/pages/streams/sidebar-context-menu/sidebar-context-menu/sidebar-context-menu.component.html +++ b/web/frontend/src/app/pages/streams/sidebar-context-menu/sidebar-context-menu/sidebar-context-menu.component.html @@ -1,11 +1,68 @@ + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -418,6 +476,7 @@ +
diff --git a/web/frontend/src/app/pages/streams/sidebar-context-menu/sidebar-context-menu/sidebar-context-menu.component.ts b/web/frontend/src/app/pages/streams/sidebar-context-menu/sidebar-context-menu/sidebar-context-menu.component.ts index 54f8607e..a53ed4d8 100644 --- a/web/frontend/src/app/pages/streams/sidebar-context-menu/sidebar-context-menu/sidebar-context-menu.component.ts +++ b/web/frontend/src/app/pages/streams/sidebar-context-menu/sidebar-context-menu/sidebar-context-menu.component.ts @@ -24,6 +24,8 @@ import { ViewsService } from '../../../../shared/servi import {ModalDescribeComponent} from '../../components/modals/modal-describe/modal-describe.component'; import {ModalExportFileComponent} from '../../components/modals/modal-export-file/modal-export-file.component'; import {ModalImportQSMSGFileComponent} from '../../components/modals/modal-import-QSMSG-file/modal-import-QSMSG-file.component'; +import {CreateStreamModalComponent} from '../../components/modals/create-stream-modal/create-stream-modal.component'; +import {CreateViewModalComponent} from '../../components/modals/create-view/create-view-modal.component'; import {ModalPurgeComponent} from '../../components/modals/modal-purge/modal-purge.component'; import {ModalRenameComponent} from '../../components/modals/modal-rename/modal-rename.component'; import {ModalSendMessageComponent} from '../../components/modals/modal-send-message/modal-send-message.component'; @@ -52,6 +54,7 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { queryParams: {[index: string]: string | string[]}; newTabQueryParams: {[index: string]: string}; item: MenuItem; + isDb: boolean; isRootSpace: boolean; isView: boolean; isTopic: boolean; @@ -60,6 +63,7 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { isNotStreamOrSpace: boolean; hasPricesL2Chart: boolean; isWriter$: Observable; + showTopics$: Observable; showPlaybackItem$ = new BehaviorSubject(true); reverseViewIsDefault$ = new BehaviorSubject(false); @ViewChild('deleteItemMessage') private deleteItemMessage: TemplateRef; @@ -89,24 +93,28 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe(({event, item}) => { this.item = item; + this.isDb = this.item.type === MenuItemType.db; this.isTopic = this.item.type === MenuItemType.topic; - const queryParams = { - chartType: this.item.meta.chartType.map(ct => ct.chartType), - chartTypeTitles: this.item.meta.chartType.map(ct => ct.title), - name: this.isTopic ? this.item.name : this.item.meta.stream.name, - isView: this.item.meta.isView ? '1' : '', - isTopic: this.isTopic ? '1' : '', - }; - this.queryParams = this.item?.meta.space - ? {...queryParams, space: this.item.meta.space.id || ''} - : queryParams; - this.newTabQueryParams = {...this.queryParams, newTab: '1'}; - this.isRootSpace = this.item.meta.space && this.item.meta.space.id === ''; - this.isView = this.item.meta.isView; - this.isNotStream = !(this.item && !this.item.meta.symbol); - this.isNotStreamOrSpace = this.isNotStream || !!this.item.meta.space; - this.isSpace = !!this.item.meta.space && !this.item.meta.symbol; - this.hasPricesL2Chart = !!this.item.meta.chartType?.find(ct => ct.chartType === ChartTypes.PRICE_LEVELS); + if (!this.isDb) { + const queryParams = { + chartType: this.item.meta.chartType.map(ct => ct.chartType), + chartTypeTitles: this.item.meta.chartType.map(ct => ct.title), + name: this.isTopic ? this.item.name : this.item.meta.stream.name, + isView: this.item.meta.isView ? '1' : '', + isTopic: this.isTopic ? '1' : '', + ...(this.item.tbId ? {tbId: this.item.tbId} : {}), + }; + this.queryParams = this.item?.meta.space + ? {...queryParams, space: this.item.meta.space.id || ''} + : queryParams; + this.newTabQueryParams = {...this.queryParams, newTab: '1'}; + this.isRootSpace = this.item.meta.space && this.item.meta.space.id === ''; + this.isView = this.item.meta.isView; + this.isNotStream = !(this.item && !this.item.meta.symbol); + this.isNotStreamOrSpace = this.isNotStream || !!this.item.meta.space; + this.isSpace = !!this.item.meta.space && !this.item.meta.symbol; + this.hasPricesL2Chart = !!this.item.meta.chartType?.find(ct => ct.chartType === ChartTypes.PRICE_LEVELS); + } this.cdRef.detectChanges(); this.contextMenuService.show.next({ @@ -137,6 +145,11 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { this.showPlaybackItem$.next(activeSessiodIds.length < 8); }) + this.showTopics$ = this.globalFiltersService.getFilters().pipe( + map((f) => f?.showTopics), + distinctUntilChanged(), + ); + this.globalFiltersService.getFilters().pipe( map((f) => f?.reverseViewIsDefault), distinctUntilChanged(), @@ -181,6 +194,7 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { this.appStore.dispatch( new StreamsActions.AskToDeleteSymbols({ streamKey: this.item.meta.stream.id, + tbId: this.item.tbId, symbols: [this.item.meta.symbol] }), ); @@ -208,13 +222,14 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { } if (this.isView) { - this.viewsService.delete(this.item.meta.stream.name).subscribe(); + this.viewsService.delete(this.item.meta.stream.name, this.item.tbId).subscribe(); } else if (this.isTopic) { this.topicService.deleteTopic(this.item.id).subscribe(); } else { this.appStore.dispatch( new StreamsActions.AskToDeleteStream({ streamKey: this.item.meta.stream.id, + tbId: this.item.tbId, ...(this.item.meta.space ? {spaceName: this.item.meta.space.id} : {}), }), ); @@ -232,7 +247,7 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { showDescribe() { this.openModal(ModalDescribeComponent, { - initialState: {stream: this.item.meta.stream, view: this.item.viewMd}, + initialState: {stream: {...this.item.meta.stream, tbId: this.item.tbId}, view: this.item.viewMd}, ignoreBackdropClick: true, class: 'wide-modal scroll-content-modal', }); @@ -240,7 +255,7 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { showSendMessage() { this.openModal(ModalSendMessageComponent, { - initialState: {stream: this.item.meta.stream}, + initialState: {stream: {...this.item.meta.stream, tbId: this.item.tbId}}, ignoreBackdropClick: true, class: 'modal-message scroll-content-modal', }); @@ -258,7 +273,7 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { this.openModal(ModalImportQSMSGFileComponent, { class: 'modal-xl', ignoreBackdropClick: true, - initialState: {stream: this.item.meta.stream.id}, + initialState: {stream: this.item.meta.stream.id, tbId: this.item.tbId}, }); } @@ -266,7 +281,10 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { this.openModal(ModalPlayBackComponent, { class: 'modal-xl', ignoreBackdropClick: true, - initialState: {stream: {id: this.item.meta.stream.id, name: this.item.meta.stream.name } }, + initialState: { + stream: { id: this.item.meta.stream.id, name: this.item.meta.stream.name }, + sourceTbId: this.item.tbId, + }, }); } @@ -275,7 +293,7 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { this.openModal(ModalImportCSVFileComponent, { class: 'modal-xl', ignoreBackdropClick: true, - initialState: {streamInput: this.item.meta.stream.name}, + initialState: {streamInput: this.item.meta.stream.name, tbId: this.item.tbId}, }); } @@ -301,7 +319,7 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { private exportToFile(exportFormat: ExportFilterFormat) { this.openModal(ModalExportFileComponent, { initialState: { - stream: this.item.meta.stream, + stream: {...this.item.meta.stream, tbId: this.item.tbId}, exportFormat, symbols: this.item.meta.symbol ? [this.item.meta.symbol] : null, }, @@ -309,8 +327,53 @@ export class SidebarContextMenuComponent implements OnInit, OnDestroy { }); } + createStreamForDb() { + this.closeContextMenu(); + this.modalService.show(CreateStreamModalComponent, { + class: 'modal-small', + ignoreBackdropClick: true, + initialState: { tbId: this.item.id }, + }); + } + + createTopicForDb() { + this.closeContextMenu(); + this.modalService.show(CreateStreamModalComponent, { + class: 'modal-small', + ignoreBackdropClick: true, + initialState: { topic: true, tbId: this.item.id }, + }); + } + + createViewForDb() { + this.closeContextMenu(); + this.modalService.show(CreateViewModalComponent, { + ignoreBackdropClick: true, + class: 'modal-xl', + initialState: { tbId: this.item.id }, + }); + } + + importFromQSMSGForDb() { + this.closeContextMenu(); + this.modalService.show(ModalImportQSMSGFileComponent, { + class: 'modal-xl', + ignoreBackdropClick: true, + initialState: { tbId: this.item.id }, + }); + } + + importFromCSVForDb() { + this.closeContextMenu(); + this.modalService.show(ModalImportCSVFileComponent, { + class: 'modal-xl', + ignoreBackdropClick: true, + initialState: { tbId: this.item.id }, + }); + } + private openModal(content: string | TemplateRef | any, options: ModalOptions): BsModalRef { - if (!this.item?.meta.stream?.id && !this.isTopic) return; + if (!this.isDb && !this.item?.meta.stream?.id && !this.isTopic) return; this.closeContextMenu(); return this.modalService.show(content, options); diff --git a/web/frontend/src/app/pages/streams/store/index.ts b/web/frontend/src/app/pages/streams/store/index.ts index 8785bcfc..ac8c3ff7 100644 --- a/web/frontend/src/app/pages/streams/store/index.ts +++ b/web/frontend/src/app/pages/streams/store/index.ts @@ -11,6 +11,7 @@ import {reducer as fromSchema, State as SchemaState} from './stream-schema/strea import {reducer as fromList, State as ListState} from './streams-list/streams.reducer'; import {reducer as fromTabs, State as TabsState} from './streams-tabs/streams-tabs.reducer'; import {reducer as fromBar, State as BarState} from './timeline-bar/timeline-bar.reducer'; +import {reducer as fromTimebases, State as TimebasesState} from './timebases/timebases.reducer'; export const streamsStoreSelector = createFeatureSelector('streams-store'); @@ -23,6 +24,7 @@ export interface StreamsState { tabs: TabsState; schema: SchemaState; query: QueryState; + timebases: TimebasesState; } export const reducers: ActionReducerMap = { @@ -34,4 +36,5 @@ export const reducers: ActionReducerMap = { tabs: fromTabs, schema: fromSchema, query: fromQuery, + timebases: fromTimebases, }; diff --git a/web/frontend/src/app/pages/streams/store/stream-details/stream-details.actions.ts b/web/frontend/src/app/pages/streams/store/stream-details/stream-details.actions.ts index 93c59f19..78441f3e 100644 --- a/web/frontend/src/app/pages/streams/store/stream-details/stream-details.actions.ts +++ b/web/frontend/src/app/pages/streams/store/stream-details/stream-details.actions.ts @@ -161,6 +161,7 @@ export class GetStreamRange implements Action { streamId: string; symbol?: string; spaceName?: string; + tbId?: string; }, ) {} } diff --git a/web/frontend/src/app/pages/streams/store/stream-details/stream-details.effects.ts b/web/frontend/src/app/pages/streams/store/stream-details/stream-details.effects.ts index 48732342..f38ad9c3 100644 --- a/web/frontend/src/app/pages/streams/store/stream-details/stream-details.effects.ts +++ b/web/frontend/src/app/pages/streams/store/stream-details/stream-details.effects.ts @@ -1,6 +1,7 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {Actions, createEffect, ofType} from '@ngrx/effects'; +import {select, Store} from '@ngrx/store'; import {Subject} from 'rxjs'; import { distinctUntilChanged, @@ -10,13 +11,17 @@ import { switchMap, takeUntil, tap, + withLatestFrom, } from 'rxjs/operators'; +import {AppState} from '../../../../core/store'; import {SchemaService} from '../../../../shared/services/schema.service'; import {StreamsService} from '../../../../shared/services/streams.service'; import {SymbolsService} from '../../../../shared/services/symbols.service'; import {TabModel} from '../../models/tab.model'; import {TabsService} from '../../services/tabs.service'; import * as FilterActions from '../filter/filter.actions'; +import {getStreamsList} from '../streams-list/streams.selectors'; +import {getActiveOrFirstTab} from '../streams-tabs/streams-tabs.selectors'; import * as StreamDetailsActions from './stream-details.actions'; import {StreamDetailsActionTypes} from './stream-details.actions'; @@ -70,7 +75,7 @@ export class StreamDetailsEffects { ofType(StreamDetailsActionTypes.GET_STREAM_RANGE), switchMap((action) => { return this.streamsService - .rangeCached(action.payload.streamId, action.payload.symbol, action.payload.spaceName) + .rangeCached(action.payload.streamId, action.payload.symbol, action.payload.spaceName, null, action.payload.tbId) .pipe(map((streamRange) => new StreamDetailsActions.SetStreamRange({streamRange}))); }), )); @@ -79,8 +84,10 @@ export class StreamDetailsEffects { ofType(StreamDetailsActionTypes.GET_SCHEMA), map((action) => action.payload.streamId), // distinctUntilChanged(), - switchMap((streamId) => { - return this.schemaService.getSchema(streamId, null, true).pipe( + withLatestFrom(this.appStore.pipe(select(getStreamsList))), + switchMap(([streamId, streams]) => { + const tbId = streams?.find(s => s.key === streamId)?.tbId; + return this.schemaService.getSchema(streamId, null, true, tbId).pipe( takeUntil(this.stop_subscription$), map((resp) => { let schemaTypes = []; @@ -116,8 +123,13 @@ export class StreamDetailsEffects { distinctUntilChanged( (e, prev) => `${e.streamId}-${e.spaceId}` === `${prev.streamId}-${prev.spaceId}`, ), - switchMap(({streamId, spaceId}) => { - return this.symbolsService.getSymbols(streamId, spaceId).pipe( + withLatestFrom( + this.appStore.pipe(select(getStreamsList)), + this.appStore.pipe(select(getActiveOrFirstTab)), + ), + switchMap(([{streamId, spaceId}, streams, activeTab]) => { + const tbId = streams?.find(s => s.key === streamId)?.tbId ?? activeTab?.tbId; + return this.symbolsService.getSymbols(streamId, spaceId, null, tbId).pipe( takeUntil(this.stop_subscription$), map((resp: Array) => { return new StreamDetailsActions.SetSymbols({ @@ -160,5 +172,6 @@ export class StreamDetailsEffects { private schemaService: SchemaService, private symbolsService: SymbolsService, private streamsService: StreamsService, + private appStore: Store, ) {} } diff --git a/web/frontend/src/app/pages/streams/store/stream-props/stream-props.effects.ts b/web/frontend/src/app/pages/streams/store/stream-props/stream-props.effects.ts index 5a5b951f..777f3534 100644 --- a/web/frontend/src/app/pages/streams/store/stream-props/stream-props.effects.ts +++ b/web/frontend/src/app/pages/streams/store/stream-props/stream-props.effects.ts @@ -20,6 +20,7 @@ export class StreamPropsEffects { return this.httpClient .get(`/${encodeURIComponent(activeTab.stream)}/options`, { headers: {customError: 'true'}, + params: activeTab.tbId ? {tb: activeTab.tbId} : {}, }) .pipe( takeUntil(this.stop_subscription$), diff --git a/web/frontend/src/app/pages/streams/store/streams-list/streams.actions.ts b/web/frontend/src/app/pages/streams/store/streams-list/streams.actions.ts index cf760b34..41c1e247 100644 --- a/web/frontend/src/app/pages/streams/store/streams-list/streams.actions.ts +++ b/web/frontend/src/app/pages/streams/store/streams-list/streams.actions.ts @@ -79,6 +79,7 @@ export class GetSymbols implements Action { constructor( public payload: { streamKey: string; + tbId?: string; spaceName?: string; props?: { _filter?: string; @@ -105,6 +106,7 @@ export class GetSpaces implements Action { constructor( public payload: { streamKey: string; + tbId?: string; props?: { _filter?: string; }; @@ -182,6 +184,7 @@ export class TruncateStream implements Action { constructor( public payload: { streamKey: string; + tbId?: string; params: { symbols?: string[]; timestamp: number; @@ -196,6 +199,7 @@ export class PurgeStream implements Action { constructor( public payload: { streamKey: string; + tbId?: string; params: { timestamp: number; }; @@ -209,6 +213,7 @@ export class AskToDeleteStream implements Action { constructor( public payload: { streamKey: string; + tbId?: string; spaceName?: string; noNotification?: boolean }, @@ -221,6 +226,7 @@ export class AskToDeleteSymbols implements Action { constructor( public payload: { streamKey: string; + tbId?: string; symbols: string[]; }, ) {} @@ -232,6 +238,7 @@ export class DeleteStream implements Action { constructor( public payload: { streamKey: string; + tbId?: string; spaceName?: string; }, ) {} @@ -261,6 +268,7 @@ export class DownloadQSMSGFile implements Action { constructor( public payload: { streamId: string; + tbId?: string; }, ) {} } @@ -271,6 +279,7 @@ export class AskToRenameStream implements Action { constructor( public payload: { streamId: string; + tbId?: string; newName: string; spaceName?: string; }, @@ -283,6 +292,7 @@ export class AskToRenameSymbol implements Action { constructor( public payload: { streamId: string; + tbId?: string; oldSymbolName: string; newSymbolName: string; spaceName?: string; @@ -296,6 +306,7 @@ export class RenameStream implements Action { constructor( public payload: { streamId: string; + tbId?: string; newName: string; spaceName?: string; }, @@ -308,6 +319,7 @@ export class RenameSymbol implements Action { constructor( public payload: { streamId: string; + tbId?: string; oldSymbolName: string; newSymbolName: string; }, @@ -320,6 +332,7 @@ export class GetStreamDescribe implements Action { constructor( public payload: { streamId: string; + tbId?: string; }, ) {} } @@ -342,6 +355,7 @@ export class SendMessage implements Action { messages: StreamDetailsModel[]; writeMode: string; streamId: string; + tbId?: string; }, ) {} } diff --git a/web/frontend/src/app/pages/streams/store/streams-list/streams.effects.ts b/web/frontend/src/app/pages/streams/store/streams-list/streams.effects.ts index 7e3079d1..316af063 100644 --- a/web/frontend/src/app/pages/streams/store/streams-list/streams.effects.ts +++ b/web/frontend/src/app/pages/streams/store/streams-list/streams.effects.ts @@ -50,6 +50,7 @@ export class StreamsEffects { return this.httpClient .get(url, { params: { + ...(action.payload.tbId ? {tb: action.payload.tbId} : {}), ...(action.payload?.props?._filter?.length ? { filter: encodeURIComponent(action.payload.props._filter), @@ -84,11 +85,12 @@ export class StreamsEffects { headers: { customError: 'true', }, - ...(action.payload.props?._filter?.length - ? { - params: {filter: encodeURIComponent(action.payload.props._filter)}, - } - : {}), + params: { + ...(action.payload.tbId ? {tb: action.payload.tbId} : {}), + ...(action.payload.props?._filter?.length + ? {filter: encodeURIComponent(action.payload.props._filter)} + : {}), + }, }) .pipe( map((resp: string[]) => { @@ -99,6 +101,7 @@ export class StreamsEffects { }) : new StreamsActions.GetSymbols({ streamKey: action.payload.streamKey, + tbId: action.payload.tbId, ...(action.payload?.props._filter?.length ? { props: {_filter: encodeURIComponent(action.payload.props._filter)}, @@ -110,6 +113,7 @@ export class StreamsEffects { return of( new StreamsActions.GetSymbols({ streamKey: action.payload.streamKey, + tbId: action.payload.tbId, ...(action.payload?.props._filter?.length ? { props: {_filter: encodeURIComponent(action.payload.props._filter)}, @@ -132,6 +136,7 @@ export class StreamsEffects { }), new StreamsActions.GetSymbols({ streamKey: action.payload.stream.key, + tbId: action.payload.stream.tbId, props: action.payload.props, ...(typeof action.payload.spaceName === 'string' ? {spaceName: action.payload.spaceName} @@ -150,6 +155,7 @@ export class StreamsEffects { }), new StreamsActions.GetSpaces({ streamKey: action.payload.stream.key, + tbId: action.payload.stream.tbId, props: action.payload.props, }), ]), @@ -161,6 +167,7 @@ export class StreamsEffects { .post( `${encodeURIComponent(action.payload.streamKey)}/truncate`, action.payload.params, + {params: action.payload.tbId ? {tb: action.payload.tbId} : {}}, ) .pipe( switchMap(() => this.translate.get('notification_messages')), @@ -185,6 +192,7 @@ export class StreamsEffects { .post( `${encodeURIComponent(action.payload.streamKey)}/purge`, action.payload.params, + {params: action.payload.tbId ? {tb: action.payload.tbId} : {}}, ) .pipe( switchMap(() => this.translate.get('notification_messages')), @@ -206,7 +214,8 @@ export class StreamsEffects { ofType(StreamsActionTypes.GET_STREAM_DESCRIBE), switchMap((action) => { return this.httpClient - .get(`${encodeURIComponent(action.payload.streamId)}/describe`) + .get(`${encodeURIComponent(action.payload.streamId)}/describe`, + {params: action.payload.tbId ? {tb: action.payload.tbId} : {}}) .pipe( map((resp) => { return new StreamsActions.SetStreamDescribe({describe: resp}); @@ -224,18 +233,13 @@ export class StreamsEffects { return ( action.payload.spaceName ? this.httpClient.get(`${encodeURIComponent(action.payload.streamKey)}${url}`, { - ...(action.payload.spaceName - ? { - // headers: { - // 'Content-Type': 'application/json', - // }, - params: { - space: action.payload.spaceName, - }, - } - : {}), + params: { + space: action.payload.spaceName, + ...(action.payload.tbId ? {tb: action.payload.tbId} : {}), + }, }) - : this.httpClient.post(`${encodeURIComponent(action.payload.streamKey)}${url}`, {}) + : this.httpClient.post(`${encodeURIComponent(action.payload.streamKey)}${url}`, {}, + {params: action.payload.tbId ? {tb: action.payload.tbId} : {}}) ).pipe( switchMap(() => this.translate.get('notification_messages')), mergeMap((messages) => { @@ -275,7 +279,8 @@ export class StreamsEffects { ofType(StreamsActionTypes.ASK_TO_DELETE_SYMBOLS), switchMap((action) => { const url = '/deleteSymbols'; - return this.httpClient.post(`${encodeURIComponent(action.payload.streamKey)}${url}`, action.payload.symbols) + return this.httpClient.post(`${encodeURIComponent(action.payload.streamKey)}${url}`, action.payload.symbols, + {params: action.payload.tbId ? {tb: action.payload.tbId} : {}}) .pipe( switchMap(() => this.translate.get('notification_messages')), mergeMap((messages) => { @@ -305,16 +310,14 @@ export class StreamsEffects { return ( action.payload.spaceName ? this.httpClient.get(`${encodeURIComponent(action.payload.streamId)}${url}`, { - ...(action.payload.spaceName - ? { - params: { - space: action.payload.spaceName, - newName: action.payload.newName, - }, - } - : {}), + params: { + space: action.payload.spaceName, + newName: action.payload.newName, + ...(action.payload.tbId ? {tb: action.payload.tbId} : {}), + }, }) - : this.httpClient.post(`${encodeURIComponent(action.payload.streamId)}${url}`, data) + : this.httpClient.post(`${encodeURIComponent(action.payload.streamId)}${url}`, data, + {params: action.payload.tbId ? {tb: action.payload.tbId} : {}}) ).pipe( switchMap(() => this.translate.get('notification_messages')), tap(() => { @@ -364,6 +367,7 @@ export class StreamsEffects { action.payload.oldSymbolName, )}/rename`, data, + {params: action.payload.tbId ? {tb: action.payload.tbId} : {}}, ) .pipe( tap(() => @@ -438,6 +442,7 @@ export class StreamsEffects { .get(`/${action.payload.streamId}/export`, { observe: 'response', responseType: 'arraybuffer', + params: action.payload.tbId ? {tb: action.payload.tbId} : {}, }) .pipe( map((resp: HttpResponse) => { @@ -458,7 +463,10 @@ export class StreamsEffects { switchMap((action) => { return this.httpClient .post(`/${encodeURIComponent(action.payload.streamId)}/write`, action.payload.messages, { - params: {writeMode: action.payload.writeMode}, + params: { + writeMode: action.payload.writeMode, + ...(action.payload.tbId ? {tb: action.payload.tbId} : {}), + }, }) .pipe( switchMap(() => this.translate.get('notification_messages')), diff --git a/web/frontend/src/app/pages/streams/store/timebases/timebases.actions.ts b/web/frontend/src/app/pages/streams/store/timebases/timebases.actions.ts new file mode 100644 index 00000000..e09846f6 --- /dev/null +++ b/web/frontend/src/app/pages/streams/store/timebases/timebases.actions.ts @@ -0,0 +1,33 @@ +import {Action} from '@ngrx/store'; +import {TimebaseInstanceDef} from '../../../../shared/models/timebase-instance-def.model'; + +export enum TimebasesActionTypes { + LOAD_TIMEBASES = '[Timebases] Load Timebases', + SET_TIMEBASES = '[Timebases] Set Timebases', + LOAD_TIMEBASES_FAILED = '[Timebases] Load Timebases Failed', + TIMEBASE_STATUS_CHANGED = '[Timebases] Timebase Status Changed', +} + +export class LoadTimebases implements Action { + readonly type = TimebasesActionTypes.LOAD_TIMEBASES; +} + +export class SetTimebases implements Action { + readonly type = TimebasesActionTypes.SET_TIMEBASES; + + constructor(public payload: { timebases: TimebaseInstanceDef[] }) {} +} + +export class LoadTimebasesFailed implements Action { + readonly type = TimebasesActionTypes.LOAD_TIMEBASES_FAILED; + + constructor(public payload: { error: any }) {} +} + +export class TimebaseStatusChanged implements Action { + readonly type = TimebasesActionTypes.TIMEBASE_STATUS_CHANGED; + + constructor(public payload: { id: string; connected: boolean; errorMessage?: string }) {} +} + +export type TimebasesActions = LoadTimebases | SetTimebases | LoadTimebasesFailed | TimebaseStatusChanged; diff --git a/web/frontend/src/app/pages/streams/store/timebases/timebases.effects.ts b/web/frontend/src/app/pages/streams/store/timebases/timebases.effects.ts new file mode 100644 index 00000000..bb72adf2 --- /dev/null +++ b/web/frontend/src/app/pages/streams/store/timebases/timebases.effects.ts @@ -0,0 +1,67 @@ +import {Injectable} from '@angular/core'; +import {Actions, createEffect, ofType} from '@ngrx/effects'; +import {catchError, map, mergeMap, switchMap} from 'rxjs/operators'; +import {of} from 'rxjs'; +import {TimebasesService} from '../../../../shared/services/timebases.service'; +import {WSService} from '../../../../core/services/ws.service'; +import * as NotificationsActions from '../../../../core/modules/notifications/store/notifications.actions'; +import * as TimebasesActions from './timebases.actions'; +import {TimebasesActionTypes} from './timebases.actions'; + +interface TimebaseStatusEvent { + id: string; + connected: boolean; + errorMessage?: string; +} + +@Injectable() +export class TimebasesEffects { + loadTimebases = createEffect(() => + this.actions$.pipe( + ofType(TimebasesActionTypes.LOAD_TIMEBASES), + switchMap(() => + this.timebasesService.getTimebases().pipe( + map((timebases) => new TimebasesActions.SetTimebases({timebases})), + catchError((error) => of(new TimebasesActions.LoadTimebasesFailed({error}))), + ), + ), + ), + ); + + timebaseStatusChanged = createEffect(() => + this.wsService.watchObject('/topic/timebase-status').pipe( + mergeMap((event) => { + const statusAction = new TimebasesActions.TimebaseStatusChanged(event); + const alias = `timebase-status-${event.id}`; + + return event.connected + ? [ + statusAction, + new NotificationsActions.RemoveWarnByAlias(alias), + new NotificationsActions.AddNotification({ + type: 'success', + message: `Timebase "${event.id}" is available again`, + dismissible: true, + closeInterval: 4000, + alias, + }), + ] + : [ + statusAction, + new NotificationsActions.AddWarn({ + type: 'warning', + message: `Timebase "${event.id}" is unavailable${event.errorMessage ? ': ' + event.errorMessage : ''}`, + dismissible: true, + alias, + }), + ]; + }), + ), + ); + + constructor( + private actions$: Actions, + private timebasesService: TimebasesService, + private wsService: WSService, + ) {} +} diff --git a/web/frontend/src/app/pages/streams/store/timebases/timebases.reducer.ts b/web/frontend/src/app/pages/streams/store/timebases/timebases.reducer.ts new file mode 100644 index 00000000..c4b0863c --- /dev/null +++ b/web/frontend/src/app/pages/streams/store/timebases/timebases.reducer.ts @@ -0,0 +1,36 @@ +import {TimebaseInstanceDef} from '../../../../shared/models/timebase-instance-def.model'; +import {TimebasesActions, TimebasesActionTypes} from './timebases.actions'; + +export interface State { + timebases: TimebaseInstanceDef[]; + loaded: boolean; +} + +export const initialState: State = { + timebases: [], + loaded: false, +}; + +export function reducer(state = initialState, action: TimebasesActions): State { + switch (action.type) { + case TimebasesActionTypes.SET_TIMEBASES: + return { + ...state, + timebases: action.payload.timebases, + loaded: true, + }; + + case TimebasesActionTypes.TIMEBASE_STATUS_CHANGED: + return { + ...state, + timebases: state.timebases.map((tb) => + tb.id === action.payload.id + ? {...tb, connected: action.payload.connected, errorMessage: action.payload.errorMessage} + : tb, + ), + }; + + default: + return state; + } +} diff --git a/web/frontend/src/app/pages/streams/store/timebases/timebases.selectors.ts b/web/frontend/src/app/pages/streams/store/timebases/timebases.selectors.ts new file mode 100644 index 00000000..7fa83959 --- /dev/null +++ b/web/frontend/src/app/pages/streams/store/timebases/timebases.selectors.ts @@ -0,0 +1,23 @@ +import {createSelector} from '@ngrx/store'; +import {StreamsState, streamsStoreSelector} from '../index'; +import {State as TimebasesState} from './timebases.reducer'; + +export const getTimebasesState = createSelector( + streamsStoreSelector, + (state: StreamsState) => state.timebases, +); + +export const getTimebases = createSelector( + getTimebasesState, + (state: TimebasesState) => state.timebases, +); + +export const getTimebasesLoaded = createSelector( + getTimebasesState, + (state: TimebasesState) => state.loaded, +); + +export const getDefaultTimebase = createSelector( + getTimebases, + (timebases) => timebases?.find((tb) => tb.connected !== false) ?? timebases?.[0] ?? null, +); diff --git a/web/frontend/src/app/pages/streams/streams-navigation/streams-navigation.service.ts b/web/frontend/src/app/pages/streams/streams-navigation/streams-navigation.service.ts index e52402f4..c225dd1d 100644 --- a/web/frontend/src/app/pages/streams/streams-navigation/streams-navigation.service.ts +++ b/web/frontend/src/app/pages/streams/streams-navigation/streams-navigation.service.ts @@ -14,7 +14,7 @@ export class StreamsNavigationService { constructor(private router: Router) {} url(item: MenuItem, activeTabType: string, reverseViewIsDefault: boolean): string[] { - if (!item || item.type === MenuItemType.group) { + if (!item || item.type === MenuItemType.group || item.type === MenuItemType.db) { return null; } @@ -59,11 +59,15 @@ export class StreamsNavigationService { if (isTopic) { params['stream'] = item.id + '#topic#'; } else { - params['stream'] = item.meta.stream.id; + params['stream'] = item.meta.stream.id; } params['symbol'] = item.meta.symbol; } + if (item.tbId) { + params['tbId'] = item.tbId; + } + return params; } diff --git a/web/frontend/src/app/pages/streams/streams.module.ts b/web/frontend/src/app/pages/streams/streams.module.ts index 25db8154..7db19885 100644 --- a/web/frontend/src/app/pages/streams/streams.module.ts +++ b/web/frontend/src/app/pages/streams/streams.module.ts @@ -78,6 +78,7 @@ import {StreamsEffects} from './store/streams-list/streams.effects'; import * as fromStreams from './store/streams-list/streams.reducer'; import {StreamsTabsEffects} from './store/streams-tabs/streams-tabs.effects'; import {TimelineBarEffects} from './store/timeline-bar/timeline-bar.effects'; +import {TimebasesEffects} from './store/timebases/timebases.effects'; import {StreamsNavigationModule} from './streams-navigation/streams-navigation.module'; import {StreamsRoutingModule} from './streams-routing.module'; import {FiltersPanelModule} from './components/filters-panel/filters-panel.module'; @@ -183,6 +184,7 @@ const DEFAULT_PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = { StreamsTabsEffects, StreamQueryEffects, SelectedMessageEffects, + TimebasesEffects, ]), PerfectScrollbarModule, AngularSplitModule, diff --git a/web/frontend/src/app/shared/components/stream-describe-content/stream-describe-content.component.ts b/web/frontend/src/app/shared/components/stream-describe-content/stream-describe-content.component.ts index 42d4d4dc..184b9af7 100644 --- a/web/frontend/src/app/shared/components/stream-describe-content/stream-describe-content.component.ts +++ b/web/frontend/src/app/shared/components/stream-describe-content/stream-describe-content.component.ts @@ -25,7 +25,7 @@ export class StreamDescribeContentComponent implements OnInit, OnChanges { ) { } ngOnInit(): void { - const ddl$ = this.streamsService.describe(this.stream.id).pipe(map(describe => describe.ddl), shareReplay(1)); + const ddl$ = this.streamsService.describe(this.stream.id, this.stream.tbId).pipe(map(describe => describe.ddl), shareReplay(1)); this.updateInitControl(); this.content$ = this.viewControl.valueChanges.pipe( startWith(null), 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 4a74118c..5cec6a3d 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 @@ -193,7 +193,7 @@ export class LiveGridComponent implements OnInit, OnDestroy, OnChanges { if (tab?.stream?.endsWith('#topic#')) { return this.topicService.getTopicSchema(tab.stream.slice(0, tab.stream.length - 7)); } else { - return tab?.stream ? this.schemaService.getSchema(tab.stream) : of(this.schemaData); + return tab?.stream ? this.schemaService.getSchema(tab.stream, null, false, tab?.tbId) : of(this.schemaData); } }), switchMap(schema => { @@ -312,7 +312,7 @@ export class LiveGridComponent implements OnInit, OnDestroy, OnChanges { this.messageInfoService.setGridApi(readyEvent); } - private runLive({symbols, types, space, fromTimestamp, destination, qql}: LiveGridFilters) { + private runLive({symbols, types, space, fromTimestamp, destination, qql, tbId}: LiveGridFilters) { this.gridReady$.pipe(take(1)).subscribe((readyEvent) => { readyEvent.api.setRowData([]); this.cleanWebsocketSubscription(); @@ -325,6 +325,7 @@ export class LiveGridComponent implements OnInit, OnDestroy, OnChanges { socketData.space = space; socketData.fromTimestamp = fromTimestamp; socketData.qql = qql; + socketData.tbId = tbId; Object.keys(socketData) .filter((key) => [undefined, null].includes(socketData[key])) .forEach((key) => delete socketData[key]); diff --git a/web/frontend/src/app/shared/models/app.info.model.ts b/web/frontend/src/app/shared/models/app.info.model.ts index 1a002213..aa07bb86 100644 --- a/web/frontend/src/app/shared/models/app.info.model.ts +++ b/web/frontend/src/app/shared/models/app.info.model.ts @@ -1,11 +1,16 @@ +export interface TimebaseInstanceModel { + id: string; + url: string; + readonly: boolean; + connected: boolean; + serverVersion: string; + clientVersion: string; +} + export interface AppInfoModel { name: string; version: string; timestamp: number; - timebase: { - clientVersion: string; - connected: boolean; - serverVersion: string; - }; + timebases: TimebaseInstanceModel[]; authentication: boolean; } diff --git a/web/frontend/src/app/shared/models/live-grid-filters.ts b/web/frontend/src/app/shared/models/live-grid-filters.ts index 111a82b4..31a791e0 100644 --- a/web/frontend/src/app/shared/models/live-grid-filters.ts +++ b/web/frontend/src/app/shared/models/live-grid-filters.ts @@ -5,4 +5,5 @@ export interface LiveGridFilters { destination: string; types: string[]; qql?: string; + tbId?: string; } diff --git a/web/frontend/src/app/shared/models/menu-item.ts b/web/frontend/src/app/shared/models/menu-item.ts index db844e47..72ecb173 100644 --- a/web/frontend/src/app/shared/models/menu-item.ts +++ b/web/frontend/src/app/shared/models/menu-item.ts @@ -7,6 +7,7 @@ export enum MenuItemType { group = 'GROUP', view = 'VIEW', topic = 'TOPIC', + db = 'DB', } export interface MenuItemMeta extends SidebarContextMenuItem { @@ -31,5 +32,8 @@ export interface MenuItem { state: string; }; active?: boolean; - parent?: string + parent?: string; + tbId?: string; + available?: boolean; + errorMessage?: string; } diff --git a/web/frontend/src/app/shared/models/timebase-instance-def.model.ts b/web/frontend/src/app/shared/models/timebase-instance-def.model.ts new file mode 100644 index 00000000..3bde8d48 --- /dev/null +++ b/web/frontend/src/app/shared/models/timebase-instance-def.model.ts @@ -0,0 +1,7 @@ +export interface TimebaseInstanceDef { + id: string; + url: string; + readonly: boolean; + connected: boolean; + errorMessage?: string; +} diff --git a/web/frontend/src/app/shared/models/view.ts b/web/frontend/src/app/shared/models/view.ts index e29e2926..7761a925 100644 --- a/web/frontend/src/app/shared/models/view.ts +++ b/web/frontend/src/app/shared/models/view.ts @@ -4,4 +4,5 @@ export interface ViewInfo { description: string; lastTimestamp: number; query: string; + tbId?: string; } diff --git a/web/frontend/src/app/shared/qql-editor/qql-editor.component.ts b/web/frontend/src/app/shared/qql-editor/qql-editor.component.ts index 356d32a8..f9dfa14c 100644 --- a/web/frontend/src/app/shared/qql-editor/qql-editor.component.ts +++ b/web/frontend/src/app/shared/qql-editor/qql-editor.component.ts @@ -10,7 +10,7 @@ import { } from '@angular/core'; import { ControlValueAccessor, UntypedFormControl, NG_VALUE_ACCESSOR, NgControl, Validators } from '@angular/forms'; import { EditorComponent } from 'ngx-monaco-editor'; -import { Observable, ReplaySubject, timer, of } from 'rxjs'; +import { BehaviorSubject, Observable, ReplaySubject, timer, of } from 'rxjs'; import { debounceTime, delay, distinctUntilChanged, filter, map, shareReplay, switchMap, take, takeUntil, withLatestFrom } from 'rxjs/operators'; import { QueryFunction } from '../../pages/query/query-function'; import { QueryService } from '../../pages/query/services/query.service'; @@ -43,6 +43,13 @@ export class QqlEditorComponent implements OnInit, AfterViewInit, OnDestroy, Con @Input() selectedRange: IRange; @Input() selectedText = ''; @Input() errorInsideSelectedText: boolean; + @Input() set tbId(id: string) { + this._inputTbId = id; + this._tbId$.next(id ?? this._storeTbId ?? null); + } + get tbId(): string { + return this._inputTbId ?? this._storeTbId ?? null; + } @Output() validUpdate = new EventEmitter(); @Output() onQueryChange = new EventEmitter<{ text: string, error: boolean }>(); @@ -61,9 +68,11 @@ export class QqlEditorComponent implements OnInit, AfterViewInit, OnDestroy, Con private contextMenuSubscription; private currentTab$: Observable; private currentTabId: string; - + private _inputTbId: string; + private _storeTbId: string; + private _tbId$ = new BehaviorSubject(null); private destroy$ = new ReplaySubject(1); - + constructor( private streamsService: StreamsService, private schemaService: SchemaService, @@ -82,6 +91,12 @@ export class QqlEditorComponent implements OnInit, AfterViewInit, OnDestroy, Con ngOnInit(): void { this.currentTab$ = this.appStore.pipe(select(getActiveOrFirstTab)); + this.currentTab$.pipe(takeUntil(this.destroy$)).subscribe((tab) => { + this._storeTbId = tab?.tbId ?? null; + if (!this._inputTbId) { + this._tbId$.next(this._storeTbId); + } + }); this.editorOptions = this.monacoQqlConfigService.options(); this.control.valueChanges .pipe(debounceTime(400), takeUntil(this.destroy$), distinctUntilChanged(), withLatestFrom(this.currentTab$)) @@ -118,12 +133,17 @@ export class QqlEditorComponent implements OnInit, AfterViewInit, OnDestroy, Con editorInit(editor) { this.setEditor.emit(editor); - const streams$ = this.streamsService - .getListWithUpdates() - .pipe(map((streams: StreamModel[]) => streams.map((stream) => stream.name))); + const streams$ = this._tbId$.pipe( + distinctUntilChanged(), + switchMap(tbId => tbId + ? this.streamsService.getList(false, null, null, tbId) + : this.streamsService.getListWithUpdates() + ), + map((streams: StreamModel[]) => streams.map((stream) => stream.name)), + ); const columns = (stream) => - this.schemaService.getSchema(stream).pipe( + this.schemaService.getSchema(stream, null, false, this.tbId).pipe( map(({types, all}) => { const result = []; const fieldNamesCount = {}; @@ -172,6 +192,10 @@ export class QqlEditorComponent implements OnInit, AfterViewInit, OnDestroy, Con ]); this.monacoQqlConfigService.init(editor, streams$, columns, functions$, dataTypes); + this._tbId$.pipe(takeUntil(this.destroy$)).subscribe(tbId => { + this.monacoQqlConfigService.tbId = tbId; + }); + this.contextMenuSubscription = editor.onContextMenu((e) => { const contextMenuElement = editor.getDomNode().querySelector(".monaco-menu-container") as HTMLElement; @@ -227,10 +251,10 @@ export class QqlEditorComponent implements OnInit, AfterViewInit, OnDestroy, Con return of(false); } - return this.queryService.compile(query).pipe( + return this.queryService.compile(query, this.tbId).pipe( delay(1000), map(response => { - + const location = response?.errorLocation; if (location) { let errorLocation = { @@ -304,10 +328,10 @@ export class QqlEditorComponent implements OnInit, AfterViewInit, OnDestroy, Con } return timer(500).pipe(switchMap(() => { - return this.queryService.compile(control.value).pipe( + return this.queryService.compile(control.value, this.tbId).pipe( delay(1000), map(response => { - + const location = response?.errorLocation; if (location) { const errorLocation = { diff --git a/web/frontend/src/app/shared/right-pane/stream-description/stream-description.component.ts b/web/frontend/src/app/shared/right-pane/stream-description/stream-description.component.ts index 3b05da6b..35528655 100644 --- a/web/frontend/src/app/shared/right-pane/stream-description/stream-description.component.ts +++ b/web/frontend/src/app/shared/right-pane/stream-description/stream-description.component.ts @@ -35,8 +35,8 @@ export class StreamDescriptionComponent implements OnInit { const tab$ = this.appStore.pipe(select(getActiveTab), filter(t => !!t)); this.title$ = tab$.pipe(map(tab => 'describeModal.title.' + (tab.isView ? 'view' : 'stream'))); this.streamName$ = tab$.pipe(map(tab => tab.streamName)); - this.stream$ = tab$.pipe(map(tab => ({id: tab.stream, name: tab.streamName}))); - this.view$ = tab$.pipe(switchMap(tab => tab.isView ? this.viewsService.get(tab.name) : of(null))); + this.stream$ = tab$.pipe(map(tab => ({id: tab.stream, name: tab.streamName, tbId: tab.tbId}))); + this.view$ = tab$.pipe(switchMap(tab => tab.isView ? this.viewsService.get(tab.name, tab.tbId) : of(null))); this.viewLoaded$ = combineLatest([this.view$, tab$]).pipe( map(([view, tab]) => !tab.isView || !!view), delay(0), diff --git a/web/frontend/src/app/shared/right-pane/streams-props/streams-props.component.html b/web/frontend/src/app/shared/right-pane/streams-props/streams-props.component.html index 1b6bd4dc..0c12136c 100644 --- a/web/frontend/src/app/shared/right-pane/streams-props/streams-props.component.html +++ b/web/frontend/src/app/shared/right-pane/streams-props/streams-props.component.html @@ -5,6 +5,18 @@

{{ 'Properties' | translate }}

+ + + + + + + + +
DB + {{ tbId }} +
+
@@ -147,4 +159,4 @@
{{ 'View Properties' | translate }}
- \ No newline at end of file + diff --git a/web/frontend/src/app/shared/right-pane/streams-props/streams-props.component.ts b/web/frontend/src/app/shared/right-pane/streams-props/streams-props.component.ts index e710f54e..ce2d6cc2 100644 --- a/web/frontend/src/app/shared/right-pane/streams-props/streams-props.component.ts +++ b/web/frontend/src/app/shared/right-pane/streams-props/streams-props.component.ts @@ -35,6 +35,8 @@ import { TabModel } from 'src/app/pages/streams/models/tab.model'; import { ChartService } from '../../services/chart-service'; import { forbiddenChars, forbiddenCharsForMessage } from '../../utils/forbiddenCharacters'; import { StructureUpdatesService } from 'src/app/pages/streams/services/structure-updates.service'; +import { getTimebases } from '../../../pages/streams/store/timebases/timebases.selectors'; +import { TimebaseInstanceDef } from '../../models/timebase-instance-def.model'; @Component({ selector: 'app-streams-props', @@ -69,6 +71,9 @@ export class StreamsPropsComponent implements OnInit, OnDestroy { periodicityForm: FormGroup; streamId: string; + private tbId: string; + tbId$: Observable; + tbUrl$: Observable; errorMessages = { periodicity: '', time_unit: '', @@ -107,11 +112,18 @@ export class StreamsPropsComponent implements OnInit, OnDestroy { this.notView$ = this.appStore.pipe(select(getActiveTab)).pipe(map(tab => !tab?.isView)); this.isWriter$ = this.permissionsService.isWriter(); this.currentTab$ = this.appStore.pipe(select(getActiveOrFirstTab)); + this.currentTab$.pipe(takeUntil(this.destroy$)).subscribe(tab => this.tbId = tab?.tbId); + + this.tbId$ = this.currentTab$.pipe(map((tab) => tab?.tbId || null)); + const timebases$ = this.appStore.pipe(select(getTimebases)); + this.tbUrl$ = combineLatest([this.tbId$, timebases$]).pipe( + map(([tbId, timebases]) => timebases?.find((tb: TimebaseInstanceDef) => tb.id === tbId)?.url || null), + ); this.streamsService.streamPropsOpened = true; this.streamsService - .getList(false) + .getList(false, null, null, this.tbId) .pipe(takeUntil(this.destroy$)) .subscribe(streams => { this.existingStreams = { @@ -152,9 +164,9 @@ export class StreamsPropsComponent implements OnInit, OnDestroy { routeParams$.pipe( withLatestFrom(this.currentTab$), filter(([params, tab]) => !!params.stream && !params.stream.endsWith('#topic#') && !tab?.isTopic), - switchMap(([params]) => params.symbol - ? this.symbolsService.getProps(params.stream, params.symbol, null, false) - : this.streamsService.getProps(params.stream, false) + switchMap(([params, tab]) => params.symbol + ? this.symbolsService.getProps(params.stream, params.symbol, null, false, tab?.tbId) + : this.streamsService.getProps(params.stream, false, tab?.tbId) ), take(1), withLatestFrom(this.globalFiltersService.getFilters()), @@ -206,19 +218,19 @@ export class StreamsPropsComponent implements OnInit, OnDestroy { distinctUntilChanged((t1: TabModel, t2) => t1.id === t2.id), switchMap(tab => { if (!tab.chart) { - return routeParams$.pipe(map(params => ({ - symbols: params.symbol ? [params.symbol] : [], stream: params.stream }))); + return routeParams$.pipe(map(params => ({ + symbols: params.symbol ? [params.symbol] : [], stream: params.stream, tbId: tab.tbId }))); } else { return this.chartService.openSymbols$.pipe( pluck(tab.id), - map(list => ({ symbols: list?.length ? list : [tab.symbol], stream: tab.stream })), + map(list => ({ symbols: list?.length ? list : [tab.symbol], stream: tab.stream, tbId: tab.tbId })), ); } }), filter(params => !!params.stream && !params.stream.endsWith('#topic#')), switchMap((params) => { - return params.symbols.length ? - forkJoin(params.symbols.map(symbol => this.symbolsService.getProps(params.stream, symbol))) + return params.symbols.length ? + forkJoin(params.symbols.map(symbol => this.symbolsService.getProps(params.stream, symbol, null, undefined, params.tbId))) .pipe( map(propList => { const reducedProps = propList.reduce((acc, { props }, i) => { @@ -228,13 +240,13 @@ export class StreamsPropsComponent implements OnInit, OnDestroy { const symbolProps = Object.fromEntries(Object.entries(props) .filter(([key]) => key.startsWith('symbol')) .map(([key, value]) => [`${key}_${i}`, value])); - + return { ...acc, ...symbolProps }; } }, {}); return { props: reducedProps }; })) - : this.streamsService.getProps(params.stream); + : this.streamsService.getProps(params.stream, undefined, params.tbId); } ), shareReplay(1), @@ -251,7 +263,7 @@ export class StreamsPropsComponent implements OnInit, OnDestroy { this.structureUpdatesService.onStreamUpdates() .pipe( filter(event => event.changed?.[0] === this.streamId && event.deleted?.[0] !== this.streamId), - switchMap(() => this.structureUpdatesService.getBackgroundTask(this.streamId)), + switchMap(() => this.structureUpdatesService.getBackgroundTask(this.streamId, this.tbId)), withLatestFrom(this.globalFiltersService.getFilters()), ) .subscribe(([info, filters]) => { @@ -357,7 +369,7 @@ export class StreamsPropsComponent implements OnInit, OnDestroy { select(getActiveTab), filter(t => !!t), distinctUntilChanged((t1, t2) => t1.id === t2.id), - switchMap(tab => this.viewsService.get(tab.name)), + switchMap(tab => this.viewsService.get(tab.name, tab.tbId)), ); this.infoFormatted$ = combineLatest([ @@ -383,13 +395,13 @@ export class StreamsPropsComponent implements OnInit, OnDestroy { stopBackgroudTask() { this.cancelButtonVisible = false; - this.structureUpdatesService.abortBackgroundTask(this.streamId) + this.structureUpdatesService.abortBackgroundTask(this.streamId, this.tbId) .pipe(take(1), takeUntil(this.destroy$)) .subscribe(() => this.currentTaskAborted = true); } private backGroundTaskProgress() { - this.structureUpdatesService.getBackgroundTask(this.streamId).pipe( + this.structureUpdatesService.getBackgroundTask(this.streamId, this.tbId).pipe( take(1), withLatestFrom(this.globalFiltersService.getFilters()), ).subscribe(([info, filters]) => { @@ -626,4 +638,4 @@ export class StreamsPropsComponent implements OnInit, OnDestroy { get intervalDigitValue() { return this.periodicityForm.get('intervalNumber').value; } -} \ No newline at end of file +} diff --git a/web/frontend/src/app/shared/right-pane/view-properties/view-properties.component.ts b/web/frontend/src/app/shared/right-pane/view-properties/view-properties.component.ts index 2851a831..9fdced00 100644 --- a/web/frontend/src/app/shared/right-pane/view-properties/view-properties.component.ts +++ b/web/frontend/src/app/shared/right-pane/view-properties/view-properties.component.ts @@ -34,7 +34,7 @@ export class ViewPropertiesComponent implements OnInit { select(getActiveTab), filter(t => !!t), distinctUntilChanged((t1, t2) => t1.id === t2.id), - switchMap(tab => this.viewsService.get(tab.streamName)), + switchMap(tab => this.viewsService.get(tab.streamName, tab.tbId)), ); this.infoFormatted$ = combineLatest([ diff --git a/web/frontend/src/app/shared/services/app-info.service.ts b/web/frontend/src/app/shared/services/app-info.service.ts index 31e2ae02..bfa50271 100644 --- a/web/frontend/src/app/shared/services/app-info.service.ts +++ b/web/frontend/src/app/shared/services/app-info.service.ts @@ -12,8 +12,8 @@ export class AppInfoService { constructor(private http: HttpClient) {} checkTimebaseVersion(version: string, appInfo: AppInfoModel) { - if (appInfo?.timebase?.clientVersion) { - const timebaseVersion = appInfo.timebase.clientVersion.split('.').map(num => +num); + if (appInfo?.timebases?.[0]?.clientVersion) { + const timebaseVersion = appInfo.timebases[0].clientVersion.split('.').map(num => +num); const targetVersion = version.split('.').map(num => +num); return timebaseVersion[0] > targetVersion[0] || @@ -25,4 +25,4 @@ export class AppInfoService { getAppInfo(): Observable { return this.http.get('/v'); } -} \ No newline at end of file +} diff --git a/web/frontend/src/app/shared/services/check-connection.service.ts b/web/frontend/src/app/shared/services/check-connection.service.ts index 19df7ffb..a3abb455 100644 --- a/web/frontend/src/app/shared/services/check-connection.service.ts +++ b/web/frontend/src/app/shared/services/check-connection.service.ts @@ -72,7 +72,7 @@ export class CheckConnectionService { }) .pipe( map((response: AppInfoModel) => - response.timebase.connected + response.timebases?.some(tb => tb.connected) ? ConnectionStatus.ok : ConnectionStatus.timebaseNotResponding, ), diff --git a/web/frontend/src/app/shared/services/export.service.ts b/web/frontend/src/app/shared/services/export.service.ts index 92d1be3e..909f4d6f 100644 --- a/web/frontend/src/app/shared/services/export.service.ts +++ b/web/frontend/src/app/shared/services/export.service.ts @@ -13,8 +13,10 @@ import {ExportFilter} from '../models/export-filter'; export class ExportService { constructor(private httpClient: HttpClient, private appStore: Store) {} - export(stream: string, filters: ExportFilter): Observable<{id: string}> { - return this.httpClient.post<{id: string}>(`/${encodeURIComponent(stream)}/export`, filters); + export(stream: string, filters: ExportFilter, tbId: string = null): Observable<{id: string}> { + return this.httpClient.post<{id: string}>(`/${encodeURIComponent(stream)}/export`, filters, { + params: tbId ? {tb: tbId} : {}, + }); } downloadUrl(exportId: string): Observable { diff --git a/web/frontend/src/app/shared/services/import.service.ts b/web/frontend/src/app/shared/services/import.service.ts index 84af17bb..5b12bd55 100644 --- a/web/frontend/src/app/shared/services/import.service.ts +++ b/web/frontend/src/app/shared/services/import.service.ts @@ -12,8 +12,9 @@ import { SchemaClassTypeModel } from '../models/schema.class.type.model'; export class ImportService { constructor(private httpClient: HttpClient, private wsService: WSService) {} - startImport(data: object): Observable { - return this.httpClient.post('/initImport', data); + startImport(data: object, tbId?: string): Observable { + const body = tbId ? {...data, tbId} : data; + return this.httpClient.post('/initImport', body); } importChunks(id: number, file: File, start = 0): Observable { diff --git a/web/frontend/src/app/shared/services/menu-items.service.ts b/web/frontend/src/app/shared/services/menu-items.service.ts index 5ff20820..7a8415b5 100644 --- a/web/frontend/src/app/shared/services/menu-items.service.ts +++ b/web/frontend/src/app/shared/services/menu-items.service.ts @@ -14,13 +14,14 @@ export class MenuItemsService { constructor(private httpClient: HttpClient) {} - getSymbolPath(stream: string, symbol: string, showSpaces = false, filter = null, views = false, filterOptions = null) { + getSymbolPath(stream: string, symbol: string, showSpaces = false, filter = null, views = false, filterOptions = null, tbId: string = null) { const filterParams = filterOptions ? { ...filterOptions, filterRootOnly: filterOptions.match === "streams", - matchExactly: !!filterOptions.matchExactly, + matchExactly: !!filterOptions.matchExactly, } : null; - return this.httpClient.post(`structure/${encodeURIComponent(stream)}/${encodeURIComponent(symbol)}`, + const tbParam = tbId ? `?tb=${encodeURIComponent(tbId)}` : ''; + return this.httpClient.post(`structure/${encodeURIComponent(stream)}/${encodeURIComponent(symbol)}${tbParam}`, { showSpaces, filter, views, filterOptions: filterParams }); } diff --git a/web/frontend/src/app/shared/services/monaco-qql-config.service.ts b/web/frontend/src/app/shared/services/monaco-qql-config.service.ts index cc152736..43b79b73 100644 --- a/web/frontend/src/app/shared/services/monaco-qql-config.service.ts +++ b/web/frontend/src/app/shared/services/monaco-qql-config.service.ts @@ -113,6 +113,7 @@ export class MonacoQqlConfigService implements OnDestroy { private havingAndRecordsAvailable$: Observable; private alterAvailable$ = new BehaviorSubject(false); private ddlQuery: boolean; + tbId: string = null; constructor( private monacoSqlTokensService: MonacoQqlTokensService, @@ -579,7 +580,7 @@ export class MonacoQqlConfigService implements OnDestroy { if (this.monacoService.currentDDLStreamDetails[streamKey]) { streamDetails$ = of(this.monacoService.currentDDLStreamDetails[streamKey]); } else { - streamDetails$ = this.streamsService.describe(streamKey) + streamDetails$ = this.streamsService.describe(streamKey, this.tbId) .pipe(pluck('ddl'), tap(details => this.monacoService.currentDDLStreamDetails[streamKey] = details)); } @@ -645,7 +646,7 @@ export class MonacoQqlConfigService implements OnDestroy { if (this.monacoService.currentDDLStreamSchema[streamKey]) { schema$ = of(this.monacoService.currentDDLStreamSchema[streamKey]); } else { - schema$ = this.schemaService.getSchema(streamKey) + schema$ = this.schemaService.getSchema(streamKey, null, false, this.tbId) .pipe(tap(schema => this.monacoService.currentDDLStreamSchema[streamKey] = schema)); } const dropAction = actionWord === QqlSequenceKeyWord.drop; @@ -694,7 +695,7 @@ export class MonacoQqlConfigService implements OnDestroy { if (this.monacoService.currentDDLStreamSchema[streamKey]) { schema$ = of(this.monacoService.currentDDLStreamSchema[streamKey]); } else { - schema$ = this.schemaService.getSchema(streamKey) + schema$ = this.schemaService.getSchema(streamKey, null, false, this.tbId) .pipe(tap(schema => this.monacoService.currentDDLStreamSchema[streamKey] = schema)); } @@ -703,7 +704,7 @@ export class MonacoQqlConfigService implements OnDestroy { if (this.monacoService.currentDDLStreamDetails[streamKey]) { streamDetails$ = of(this.monacoService.currentDDLStreamDetails[streamKey]); } else { - streamDetails$ = this.streamsService.describe(streamKey) + streamDetails$ = this.streamsService.describe(streamKey, this.tbId) .pipe(pluck('ddl'), tap(details => this.monacoService.currentDDLStreamDetails[streamKey] = details)); } } else { @@ -744,7 +745,7 @@ export class MonacoQqlConfigService implements OnDestroy { if (this.monacoService.currentDDLStreamSchema[streamKey]) { schema$ = of(this.monacoService.currentDDLStreamSchema[streamKey]); } else { - schema$ = this.schemaService.getSchema(streamKey) + schema$ = this.schemaService.getSchema(streamKey, null, false, this.tbId) .pipe(tap(schema => this.monacoService.currentDDLStreamSchema[streamKey] = schema)); } @@ -755,7 +756,7 @@ export class MonacoQqlConfigService implements OnDestroy { let allProperties$: Observable<[string, string][]>; if (lastKeyWord === QqlSequenceKeyWord.stream) { - allProperties$ = this.streamsService.getProps(streamKey, true).pipe( + allProperties$ = this.streamsService.getProps(streamKey, true, this.tbId).pipe( take(1), map(({props}) => Object.entries(props) .filter(([key]) => !key.includes('range')) @@ -825,14 +826,14 @@ export class MonacoQqlConfigService implements OnDestroy { const rewrite = splittedText[splittedText.length - 3].trim() === QqlSequenceKeyWord56.rewrite; const streamKey = streams[0].replace(/\"/g, ''); - const schema$ = this.schemaService.getSchema(streamKey); + const schema$ = this.schemaService.getSchema(streamKey, null, false, this.tbId); let streamDetails$: Observable; if (rewrite) { if (this.monacoService.currentDDLStreamDetails[streamKey]) { streamDetails$ = of(this.monacoService.currentDDLStreamDetails[streamKey]); } else { - streamDetails$ = this.streamsService.describe(streamKey) + streamDetails$ = this.streamsService.describe(streamKey, this.tbId) .pipe(pluck('ddl'), tap(details => this.monacoService.currentDDLStreamDetails[streamKey] = details)); } } else { @@ -890,7 +891,7 @@ export class MonacoQqlConfigService implements OnDestroy { if (this.monacoService.currentDDLStreamSchema[streamKey]) { schema$ = of(this.monacoService.currentDDLStreamSchema[streamKey]); } else { - schema$ = this.schemaService.getSchema(streamKey) + schema$ = this.schemaService.getSchema(streamKey, null, false, this.tbId) .pipe(tap(schema => this.monacoService.currentDDLStreamSchema[streamKey] = schema)); } } @@ -910,7 +911,7 @@ export class MonacoQqlConfigService implements OnDestroy { if (this.monacoService.currentDDLStreamDetails[streamKey]) { streamDetails$ = of(this.monacoService.currentDDLStreamDetails[streamKey]); } else { - streamDetails$ = this.streamsService.describe(streamKey) + streamDetails$ = this.streamsService.describe(streamKey, this.tbId) .pipe(pluck('ddl'), tap(details => this.monacoService.currentDDLStreamDetails[streamKey] = details)); } } else { @@ -1245,7 +1246,7 @@ export class MonacoQqlConfigService implements OnDestroy { } const streamKey = textBeforeCursor.match(/"([^"]+)"/)?.[1]; - const schema$ = this.schemaService.getSchema(streamKey); + const schema$ = this.schemaService.getSchema(streamKey, null, false, this.tbId); return schema$.pipe( map(schema => { const classItem = schema.all.find(cl => cl.name.endsWith(className)); @@ -1872,4 +1873,4 @@ export class MonacoQqlConfigService implements OnDestroy { function camelToSnakeCase(str: string) { return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); -} \ No newline at end of file +} diff --git a/web/frontend/src/app/shared/services/schema.service.ts b/web/frontend/src/app/shared/services/schema.service.ts index 13c962c5..b2c855dd 100644 --- a/web/frontend/src/app/shared/services/schema.service.ts +++ b/web/frontend/src/app/shared/services/schema.service.ts @@ -11,20 +11,21 @@ export class SchemaService { schema: { types: SchemaTypeModel[], all: SchemaTypeModel[] }; constructor(private httpClient: HttpClient, private cacheRequestService: CacheRequestService) {} - getSchema(stream: string, spaceId: string = null, tree = false) { + getSchema(stream: string, spaceId: string = null, tree = false, tbId: string = null) { const params = { ...(typeof spaceId === 'string' ? { space: encodeURIComponent(spaceId), } : {}), + ...(tbId ? {tb: tbId} : {}), }; if (tree) { params['tree'] = 'true'; } return this.cacheRequestService.cache( - {action: 'getSchema', stream, spaceId, tree}, + {action: 'getSchema', stream, spaceId, tree, tbId}, this.httpClient.get<{types: SchemaTypeModel[]; all: SchemaAllTypeModel[]}>( `/${encodeURIComponent(stream)}/schema`, { diff --git a/web/frontend/src/app/shared/services/stream-message.service.ts b/web/frontend/src/app/shared/services/stream-message.service.ts index cda25d7e..d2853ce9 100644 --- a/web/frontend/src/app/shared/services/stream-message.service.ts +++ b/web/frontend/src/app/shared/services/stream-message.service.ts @@ -11,12 +11,13 @@ export class StreamMessageService { streamId: string, messages: object[], writeMode: string, + tbId?: string, ): Observable<{error: string; message: string}[]> { return this.httpClient.post<{error: string; message: string}[]>( `/${encodeURIComponent(streamId)}/write`, messages, { - params: {writeMode}, + params: {writeMode, ...(tbId ? {tb: tbId} : {})}, }, ); } @@ -25,6 +26,7 @@ export class StreamMessageService { streamId: string, updatedMessage: object, messageInfo: editedMessageProps, + tbId?: string, ): Observable<{error: string; message: string}[]> { const params = {}; Object.keys(messageInfo).forEach(key => { @@ -32,6 +34,7 @@ export class StreamMessageService { params[key] = messageInfo[key]; } }) + if (tbId) params['tb'] = tbId; return this.httpClient.post<{error: string; message: string}[]>( `/${encodeURIComponent(streamId)}/update`, updatedMessage, diff --git a/web/frontend/src/app/shared/services/streams.service.ts b/web/frontend/src/app/shared/services/streams.service.ts index 1fd82a5b..f54616e8 100644 --- a/web/frontend/src/app/shared/services/streams.service.ts +++ b/web/frontend/src/app/shared/services/streams.service.ts @@ -28,7 +28,7 @@ export class StreamsService { streamNameUpdated = new Subject(); nonExistentStreamNavigated = new Subject(); - streamCreationData: { storageVersion: string, distributionFactor: number }; + streamCreationData: { storageVersion: string, distributionFactor: number, tbId?: string }; streamRemoved = new Subject(); streamPropsOpened: boolean; private rangeRequestsInProgress = {}; @@ -56,11 +56,12 @@ export class StreamsService { symbol: string = null, spaceName: string = null, barSize = null, + tbId: string = null, ): Observable<{end: string; start: string}> { const key = stream + symbol + spaceName + barSize; return this.httpClient .get<{end: string; start: string}>(`/${encodeURIComponent(stream)}/range`, { - params: this.rangeParams(symbol, spaceName, barSize), + params: {...this.rangeParams(symbol, spaceName, barSize), ...(tbId ? {tb: tbId} : {})}, }) .pipe( map(({start, end}) => { @@ -88,15 +89,16 @@ export class StreamsService { symbol: string, spaceName: string, barSize = null, + tbId: string = null, ): Observable<{end: string; start: string}> { if (!stream) { return of(null); } - const key = stream + symbol + spaceName + barSize; + const key = stream + symbol + spaceName + barSize + (tbId || ''); if (this.cashedRanges[key]) { return of(this.cashedRanges[key]); } else { - return this.range(stream, symbol, spaceName, barSize) + return this.range(stream, symbol, spaceName, barSize, tbId) .pipe(tap(range => this.cashedRanges[key] = range)); } } @@ -113,7 +115,7 @@ export class StreamsService { return this.listWithUpdates$; } - getList(useCache, filter: string = null, spaces: boolean = null): Observable { + getList(useCache, filter: string = null, spaces: boolean = null, tbId: string = null): Observable { const params = []; if (filter?.length) { params.push(`filter=${encodeURIComponent(filter)}`); @@ -123,6 +125,10 @@ export class StreamsService { params.push('spaces=true'); } + if (tbId) { + params.push(`tb=${encodeURIComponent(tbId)}`); + } + const req = '/streams' + (params.length ? `?${params.join('&')}` : ''); const canGetCache = !params.length; @@ -150,16 +156,18 @@ export class StreamsService { return streams$; } - getProps(stream: string, fromCache: boolean = true): Observable { + getProps(stream: string, fromCache: boolean = true, tbId: string = null): Observable { + const params = tbId ? {tb: tbId} : {}; const props$ = this.httpClient .get(`/${encodeURIComponent(stream)}/options`, { headers: {customError: 'true'}, + params, }) .pipe(map((resp) => ({props: resp || null, opened: false}))); if (fromCache) { return this.cacheRequestService.cache( - {action: 'StreamsService.getProps', stream}, + {action: 'StreamsService.getProps', stream, tbId}, props$, ); } else { @@ -167,8 +175,9 @@ export class StreamsService { } } - describe(streamId: string): Observable { - return this.httpClient.get(`${encodeURIComponent(streamId)}/describe`); + describe(streamId: string, tbId: string = null): Observable { + const params = tbId ? {tb: tbId} : {}; + return this.httpClient.get(`${encodeURIComponent(streamId)}/describe`, {params}); } private rangeParams(symbol: string, spaceName: string, barSize: number) { @@ -179,8 +188,9 @@ export class StreamsService { }; } - updateStreamProperties(streamId: string, props) { - return this.httpClient.put(`${encodeURIComponent(streamId)}/options`, props); + updateStreamProperties(streamId: string, props, tbId: string = null) { + const params = tbId ? {tb: tbId} : {}; + return this.httpClient.put(`${encodeURIComponent(streamId)}/options`, props, {params}); } getChartSettings(key: string) { diff --git a/web/frontend/src/app/shared/services/symbols.service.ts b/web/frontend/src/app/shared/services/symbols.service.ts index 5b555dff..aa46a3ef 100644 --- a/web/frontend/src/app/shared/services/symbols.service.ts +++ b/web/frontend/src/app/shared/services/symbols.service.ts @@ -18,16 +18,20 @@ export class SymbolsService { constructor(private httpClient: HttpClient, private cacheRequestService: CacheRequestService) {} - getSymbols(stream: string, spaceId: string = null, filter: string = null): Observable { + getSymbols(stream: string, spaceId: string = null, filter: string = null, tbId: string = null): Observable { const params: { [index: string]: string | string[] } = {}; if (typeof spaceId === 'string') { params.space = encodeURIComponent(spaceId); } - + if (filter) { params.filter = filter; } - + + if (tbId) { + params.tb = tbId; + } + return this.httpClient.get(`/${encodeURIComponent(stream)}/symbols`, { params, headers: {customError: 'true'}, @@ -40,16 +44,21 @@ export class SymbolsService { }); } - getProps(stream: string, symbol: string, time = null, fromCache = true): Observable { + getProps(stream: string, symbol: string, time = null, fromCache = true, tbId: string = null): Observable { + const params: { [index: string]: string } = {}; + if (tbId) { + params.tb = tbId; + } const props$ = this.httpClient .get(`/${encodeURIComponent(stream)}/options/${encodeURIComponent(symbol)}`, { + params, headers: {customError: 'true'}, }) .pipe(map((resp) => ({props: resp || null, opened: false}))); if (fromCache) { return this.cacheRequestService.cache( - {action: 'SymbolsService.getProps', stream, symbol}, + {action: 'SymbolsService.getProps', stream, symbol, tbId}, props$, time, ); @@ -58,14 +67,17 @@ export class SymbolsService { } } - getRanges(stream: string, symbols: string[]): Observable<{ start: string, end: string }> { - const cashKey = JSON.stringify( { stream, symbols } ); + getRanges(stream: string, symbols: string[], tbId: string = null): Observable<{ start: string, end: string }> { + const cashKey = JSON.stringify( { stream, symbols, tbId } ); if (this.rangeRequestsInProgress[cashKey] && Date.now() - this.lastRangeRequestTimestamp < 5000) { return this.rangeRequestsInProgress[cashKey]; } else { - const params = { symbols }; + const params: { [key: string]: string | string[] } = { symbols }; + if (tbId) { + params.tb = tbId; + } this.lastRangeRequestTimestamp = Date.now(); - this.rangeRequestsInProgress[cashKey] = + this.rangeRequestsInProgress[cashKey] = this.httpClient.get<{ start: string, end: string }>(`/${encodeURIComponent(stream)}/range`, { params }).pipe(shareReplay(1)); return this.rangeRequestsInProgress[cashKey]; } diff --git a/web/frontend/src/app/shared/services/timebases.service.ts b/web/frontend/src/app/shared/services/timebases.service.ts new file mode 100644 index 00000000..d5ebcd07 --- /dev/null +++ b/web/frontend/src/app/shared/services/timebases.service.ts @@ -0,0 +1,15 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {TimebaseInstanceDef} from '../models/timebase-instance-def.model'; + +@Injectable({ + providedIn: 'root', +}) +export class TimebasesService { + constructor(private httpClient: HttpClient) {} + + getTimebases(): Observable { + return this.httpClient.get('/timebases'); + } +} diff --git a/web/frontend/src/app/shared/services/views.service.ts b/web/frontend/src/app/shared/services/views.service.ts index b9e88ac9..313b893e 100644 --- a/web/frontend/src/app/shared/services/views.service.ts +++ b/web/frontend/src/app/shared/services/views.service.ts @@ -11,21 +11,38 @@ export class ViewsService { constructor(private httpClient: HttpClient) { } - getViews() { - return this.httpClient.get('/timebase/views').pipe(shareReplay(1)); + getViews(tbId?: string) { + const params = tbId ? {tb: tbId} : {}; + return this.httpClient.get('/timebase/views', {params}).pipe(shareReplay(1)); } - - save(id: string, query: string, live: boolean): Observable { - return this.httpClient.post('/timebase/views', {id, query, live}, {headers: {customError: 'true'}}).pipe(mapTo(null)); + + save(id: string, query: string, live: boolean, tbId?: string): Observable { + const params = tbId ? {tb: tbId} : {}; + return this.httpClient.post('/timebase/views', {id, query, live}, {headers: {customError: 'true'}, params}).pipe(mapTo(null)); } - - get(id: string): Observable { + + get(id: string, tbId?: string): Observable { + const idParam = encodeURIComponent(id); + const params = tbId ? {tb: tbId} : {}; + return this.httpClient.get(`/timebase/views/${idParam}`, {params}); + } + + delete(id: string, tbId?: string): Observable { const idParam = encodeURIComponent(id); - return this.httpClient.get(`/timebase/views/${idParam}`); + const params = tbId ? {tb: tbId} : {}; + return this.httpClient.delete(`/timebase/views/${idParam}`, {params}).pipe(mapTo(null)); } - - delete(id: string): Observable { + + restart(id: string, tbId?: string, from?: string): Observable { + const idParam = encodeURIComponent(id); + const params: Record = tbId ? {tb: tbId} : {}; + if (from) params['from'] = from; + return this.httpClient.put(`/timebase/views/${idParam}/restart`, null, {params}).pipe(mapTo(null)); + } + + stop(id: string, tbId?: string): Observable { const idParam = encodeURIComponent(id); - return this.httpClient.delete(`/timebase/views/${idParam}`).pipe(mapTo(null)); + const params = tbId ? {tb: tbId} : {}; + return this.httpClient.put(`/timebase/views/${idParam}/stop`, null, {params}).pipe(mapTo(null)); } } diff --git a/web/frontend/src/assets/i18n/en.json b/web/frontend/src/assets/i18n/en.json index eb52409d..f635173d 100644 --- a/web/frontend/src/assets/i18n/en.json +++ b/web/frontend/src/assets/i18n/en.json @@ -208,7 +208,10 @@ "revert": "Revert", "closeTab": "Close tab", "expand": "Expand", - "collapse": "Collapse" + "collapse": "Collapse", + "create_stream": "Create Stream", + "create_topic": "Create Topic", + "create_view": "Create View" }, "tooltips": { "query_mode": "Query mode", @@ -555,6 +558,7 @@ }, "orderBook": { "filterLabels": { + "timebase": "Timebase", "streams": "Streams", "symbol": "Symbol", "exchanges": "Exchanges", @@ -687,7 +691,8 @@ "tooltips": { "dateFormat": "Date Format", "timeFormat": "Time Format", - "timeZone": "Time Zone" + "timeZone": "Time Zone", + "showTopicsComingSoon": "Coming soon" } }, "streamList": {