Multi workspace config - still not finished

This commit is contained in:
litoral05
2026-06-09 15:35:26 +01:00
parent 6ef1e83e63
commit 6ccef04914
10 changed files with 456 additions and 99 deletions
@@ -13,9 +13,18 @@ public class ChartWorkspace {
private Integer id; private Integer id;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(nullable = false, unique = true) @Column(nullable = false)
private ChartWorkspaceScope scope; private ChartWorkspaceScope scope;
@Column(nullable = false, length = 120)
private String name;
@Column(name = "sort_order", nullable = false)
private Integer sortOrder = 0;
@Column(name = "is_default", nullable = false)
private Boolean defaultWorkspace = false;
@Column(name = "layout_mode", nullable = false) @Column(name = "layout_mode", nullable = false)
private String layoutMode; private String layoutMode;
@@ -33,10 +42,12 @@ public class ChartWorkspace {
public ChartWorkspace( public ChartWorkspace(
ChartWorkspaceScope scope, ChartWorkspaceScope scope,
String name,
String layoutMode, String layoutMode,
String chartsJson String chartsJson
) { ) {
this.scope = scope; this.scope = scope;
this.name = name;
this.layoutMode = layoutMode; this.layoutMode = layoutMode;
this.chartsJson = chartsJson; this.chartsJson = chartsJson;
} }
@@ -71,6 +82,30 @@ public class ChartWorkspace {
this.scope = scope; this.scope = scope;
} }
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
public Boolean getDefaultWorkspace() {
return defaultWorkspace;
}
public void setDefaultWorkspace(Boolean defaultWorkspace) {
this.defaultWorkspace = defaultWorkspace;
}
public String getLayoutMode() { public String getLayoutMode() {
return layoutMode; return layoutMode;
} }
@@ -4,6 +4,8 @@ import com.litoralregas.backend.charts.dto.ChartWorkspaceRequest;
import com.litoralregas.backend.charts.dto.ChartWorkspaceResponse; import com.litoralregas.backend.charts.dto.ChartWorkspaceResponse;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController @RestController
@RequestMapping("/api/chart-workspaces") @RequestMapping("/api/chart-workspaces")
public class ChartWorkspaceController { public class ChartWorkspaceController {
@@ -16,6 +18,54 @@ public class ChartWorkspaceController {
this.service = service; this.service = service;
} }
@GetMapping
public List<ChartWorkspaceResponse> listWorkspaces(
@RequestParam ChartWorkspaceScope scope
) {
return service.listWorkspaces(scope);
}
@PostMapping
public ChartWorkspaceResponse createWorkspace(
@RequestParam ChartWorkspaceScope scope,
@RequestBody ChartWorkspaceRequest request
) {
return service.createWorkspace(
scope,
request
);
}
@GetMapping("/id/{id}")
public ChartWorkspaceResponse getWorkspaceById(
@PathVariable Integer id
) {
return service.getWorkspaceById(id);
}
@PutMapping("/id/{id}")
public ChartWorkspaceResponse updateWorkspaceById(
@PathVariable Integer id,
@RequestBody ChartWorkspaceRequest request
) {
return service.updateWorkspace(
id,
request
);
}
@DeleteMapping("/id/{id}")
public void deleteWorkspaceById(
@PathVariable Integer id
) {
service.deleteWorkspace(id);
}
@GetMapping("/{scope}") @GetMapping("/{scope}")
public ChartWorkspaceResponse getWorkspace( public ChartWorkspaceResponse getWorkspace(
@PathVariable ChartWorkspaceScope scope @PathVariable ChartWorkspaceScope scope
@@ -2,12 +2,21 @@ package com.litoralregas.backend.charts;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface ChartWorkspaceRepository public interface ChartWorkspaceRepository
extends JpaRepository<ChartWorkspace, Integer> { extends JpaRepository<ChartWorkspace, Integer> {
Optional<ChartWorkspace> findByScope( List<ChartWorkspace> findAllByScopeOrderBySortOrderAscIdAsc(
ChartWorkspaceScope scope
);
Optional<ChartWorkspace> findFirstByScopeAndDefaultWorkspaceTrue(
ChartWorkspaceScope scope
);
long countByScope(
ChartWorkspaceScope scope ChartWorkspaceScope scope
); );
} }
@@ -1,19 +1,31 @@
package com.litoralregas.backend.charts; package com.litoralregas.backend.charts;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.litoralregas.backend.charts.dto.ChartWorkspaceRequest; import com.litoralregas.backend.charts.dto.ChartWorkspaceRequest;
import com.litoralregas.backend.charts.dto.ChartWorkspaceResponse; import com.litoralregas.backend.charts.dto.ChartWorkspaceResponse;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@Service @Service
public class ChartWorkspaceService { public class ChartWorkspaceService {
private static final int MAX_WORKSPACES_PER_SCOPE = 10;
private static final int MAX_CHARTS_PER_WORKSPACE = 10;
private final ChartWorkspaceRepository repository; private final ChartWorkspaceRepository repository;
private final ObjectMapper objectMapper;
public ChartWorkspaceService( public ChartWorkspaceService(
ChartWorkspaceRepository repository ChartWorkspaceRepository repository,
ObjectMapper objectMapper
) { ) {
this.repository = repository; this.repository = repository;
this.objectMapper = objectMapper;
} }
@Transactional @Transactional
@@ -21,24 +33,11 @@ public class ChartWorkspaceService {
ChartWorkspaceScope scope, ChartWorkspaceScope scope,
ChartWorkspaceRequest request ChartWorkspaceRequest request
) { ) {
ChartWorkspace workspace = ChartWorkspace workspace =
repository.findByScope(scope) repository.findFirstByScopeAndDefaultWorkspaceTrue(scope)
.orElseGet(() -> .orElseGet(() -> createDefaultWorkspace(scope));
new ChartWorkspace(
scope,
request.layoutMode(),
request.chartsJson()
)
);
workspace.setLayoutMode( applyRequest(workspace, request, true);
request.layoutMode()
);
workspace.setChartsJson(
request.chartsJson()
);
ChartWorkspace saved = ChartWorkspace saved =
repository.save(workspace); repository.save(workspace);
@@ -49,19 +48,275 @@ public class ChartWorkspaceService {
public ChartWorkspaceResponse getWorkspace( public ChartWorkspaceResponse getWorkspace(
ChartWorkspaceScope scope ChartWorkspaceScope scope
) { ) {
return toResponse(getOrCreateDefaultWorkspace(scope));
}
public List<ChartWorkspaceResponse> listWorkspaces(
ChartWorkspaceScope scope
) {
ensureDefaultWorkspace(scope);
return repository.findAllByScopeOrderBySortOrderAscIdAsc(scope)
.stream()
.map(this::toResponse)
.toList();
}
public ChartWorkspaceResponse getWorkspaceById(
Integer id
) {
return toResponse(requireWorkspace(id));
}
@Transactional
public ChartWorkspaceResponse createWorkspace(
ChartWorkspaceScope scope,
ChartWorkspaceRequest request
) {
if (repository.countByScope(scope) >= MAX_WORKSPACES_PER_SCOPE) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Maximum workspaces reached for this scope."
);
}
ChartWorkspace workspace = ChartWorkspace workspace =
repository.findByScope(scope)
.orElseGet(() ->
repository.save(
new ChartWorkspace( new ChartWorkspace(
scope, scope,
"fourGrid", normalizeName(request.name(), "Novo Workspace"),
"[]" normalizeLayoutMode(request.layoutMode()),
) normalizeChartsJson(request.chartsJson())
)
); );
return toResponse(workspace); workspace.setSortOrder(
request.sortOrder() == null
? nextSortOrder(scope)
: request.sortOrder()
);
workspace.setDefaultWorkspace(false);
if (Boolean.TRUE.equals(request.defaultWorkspace())) {
setDefaultWorkspace(workspace);
}
ChartWorkspace saved =
repository.save(workspace);
return toResponse(saved);
}
@Transactional
public ChartWorkspaceResponse updateWorkspace(
Integer id,
ChartWorkspaceRequest request
) {
ChartWorkspace workspace =
requireWorkspace(id);
applyRequest(workspace, request, false);
ChartWorkspace saved =
repository.save(workspace);
return toResponse(saved);
}
@Transactional
public void deleteWorkspace(
Integer id
) {
ChartWorkspace workspace =
requireWorkspace(id);
repository.delete(workspace);
if (Boolean.TRUE.equals(workspace.getDefaultWorkspace())) {
repository.findAllByScopeOrderBySortOrderAscIdAsc(workspace.getScope())
.stream()
.findFirst()
.ifPresent(this::setDefaultWorkspace);
}
}
private ChartWorkspace getOrCreateDefaultWorkspace(
ChartWorkspaceScope scope
) {
return repository.findFirstByScopeAndDefaultWorkspaceTrue(scope)
.orElseGet(() ->
repository.findAllByScopeOrderBySortOrderAscIdAsc(scope)
.stream()
.findFirst()
.map(workspace -> {
setDefaultWorkspace(workspace);
return repository.save(workspace);
})
.orElseGet(() ->
repository.save(createDefaultWorkspace(scope))
)
);
}
private void ensureDefaultWorkspace(
ChartWorkspaceScope scope
) {
getOrCreateDefaultWorkspace(scope);
}
private ChartWorkspace createDefaultWorkspace(
ChartWorkspaceScope scope
) {
ChartWorkspace workspace =
new ChartWorkspace(
scope,
"Workspace principal",
"fourGrid",
"[]"
);
workspace.setDefaultWorkspace(true);
workspace.setSortOrder(0);
return workspace;
}
private ChartWorkspace requireWorkspace(
Integer id
) {
return repository.findById(id)
.orElseThrow(() ->
new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Workspace not found."
)
);
}
private void applyRequest(
ChartWorkspace workspace,
ChartWorkspaceRequest request,
boolean legacyDefaultUpdate
) {
if (request.name() != null || legacyDefaultUpdate) {
workspace.setName(
normalizeName(request.name(), workspace.getName())
);
}
if (request.sortOrder() != null) {
workspace.setSortOrder(request.sortOrder());
}
if (Boolean.TRUE.equals(request.defaultWorkspace())) {
setDefaultWorkspace(workspace);
} else if (
Boolean.FALSE.equals(request.defaultWorkspace()) &&
!Boolean.TRUE.equals(workspace.getDefaultWorkspace())
) {
workspace.setDefaultWorkspace(false);
}
workspace.setLayoutMode(
normalizeLayoutMode(request.layoutMode())
);
workspace.setChartsJson(
normalizeChartsJson(request.chartsJson())
);
}
private void setDefaultWorkspace(
ChartWorkspace workspace
) {
repository.findAllByScopeOrderBySortOrderAscIdAsc(workspace.getScope())
.forEach(candidate -> {
if (!candidate.getId().equals(workspace.getId())) {
candidate.setDefaultWorkspace(false);
}
});
workspace.setDefaultWorkspace(true);
}
private int nextSortOrder(
ChartWorkspaceScope scope
) {
return repository.findAllByScopeOrderBySortOrderAscIdAsc(scope)
.stream()
.map(ChartWorkspace::getSortOrder)
.filter(value -> value != null)
.max(Integer::compareTo)
.orElse(-1) + 1;
}
private String normalizeName(
String name,
String fallback
) {
String normalized =
name == null ? "" : name.trim();
if (normalized.isBlank()) {
return fallback == null || fallback.isBlank()
? "Workspace"
: fallback;
}
if (normalized.length() > 120) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Workspace name is too long."
);
}
return normalized;
}
private String normalizeLayoutMode(
String layoutMode
) {
if (layoutMode == null || layoutMode.isBlank()) {
return "fourGrid";
}
return layoutMode;
}
private String normalizeChartsJson(
String chartsJson
) {
String normalized =
chartsJson == null || chartsJson.isBlank()
? "[]"
: chartsJson;
try {
JsonNode root =
objectMapper.readTree(normalized);
if (!root.isArray()) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"chartsJson must be an array."
);
}
if (root.size() > MAX_CHARTS_PER_WORKSPACE) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Maximum charts reached for this workspace."
);
}
} catch (ResponseStatusException exception) {
throw exception;
} catch (Exception exception) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"chartsJson is invalid."
);
}
return normalized;
} }
private ChartWorkspaceResponse toResponse( private ChartWorkspaceResponse toResponse(
@@ -71,6 +326,9 @@ public class ChartWorkspaceService {
return new ChartWorkspaceResponse( return new ChartWorkspaceResponse(
workspace.getId(), workspace.getId(),
workspace.getScope(), workspace.getScope(),
workspace.getName(),
workspace.getSortOrder(),
workspace.getDefaultWorkspace(),
workspace.getLayoutMode(), workspace.getLayoutMode(),
workspace.getChartsJson(), workspace.getChartsJson(),
workspace.getCreatedAt(), workspace.getCreatedAt(),
@@ -1,6 +1,9 @@
package com.litoralregas.backend.charts.dto; package com.litoralregas.backend.charts.dto;
public record ChartWorkspaceRequest( public record ChartWorkspaceRequest(
String name,
Integer sortOrder,
Boolean defaultWorkspace,
String layoutMode, String layoutMode,
String chartsJson String chartsJson
) { ) {
@@ -7,6 +7,9 @@ import java.time.Instant;
public record ChartWorkspaceResponse( public record ChartWorkspaceResponse(
Integer id, Integer id,
ChartWorkspaceScope scope, ChartWorkspaceScope scope,
String name,
Integer sortOrder,
Boolean defaultWorkspace,
String layoutMode, String layoutMode,
String chartsJson, String chartsJson,
Instant createdAt, Instant createdAt,
@@ -1,60 +0,0 @@
package com.litoralregas.backend.vnc.rfb;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketTimeoutException;
public class GreetClient implements AutoCloseable {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public void startConnection(String ip, int port) throws IOException {
clientSocket = new Socket(ip, port);
clientSocket.setSoTimeout(10000);
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
}
public String sendMessage(String msg) throws IOException {
if (out == null || in == null) {
throw new IllegalStateException("Connection not started");
}
out.println(msg);
String response = in.readLine();
if (response == null) {
throw new SocketTimeoutException("No response from server");
}
return response;
}
public void stopConnection() {
try {
if (in != null) in.close();
} catch (IOException ignored) {
}
if (out != null) {
out.close();
}
try {
if (clientSocket != null && !clientSocket.isClosed()) {
clientSocket.close();
}
} catch (IOException ignored) {
}
}
@Override
public void close() {
stopConnection();
}
}
@@ -109,7 +109,7 @@ public class RfbProtoDesktop {
this.sock.setKeepAlive(true); this.sock.setKeepAlive(true);
this.sock.setReuseAddress(true); this.sock.setReuseAddress(true);
this.sock.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT_MS); this.sock.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT_MS);
this.sock.setSoTimeout(READ_TIMEOUT_MS); this.sock.setSoTimeout(0);
// Increase receive buffer — large ZRLE/Tight frames arrive in bursts. // Increase receive buffer — large ZRLE/Tight frames arrive in bursts.
this.sock.setReceiveBufferSize(65536); this.sock.setReceiveBufferSize(65536);
+1 -1
View File
@@ -43,7 +43,7 @@ litoralregas:
modules: modules:
climate: climate:
enabled: true enabled: false
exterior-enabled: true exterior-enabled: true
enabled-sites: enabled-sites:
- 1 - 1
@@ -0,0 +1,59 @@
ALTER TABLE chart_workspace
ADD COLUMN name VARCHAR(120) NOT NULL DEFAULT 'Workspace principal';
ALTER TABLE chart_workspace
ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
ALTER TABLE chart_workspace
ADD COLUMN is_default BOOLEAN NOT NULL DEFAULT TRUE;
CREATE TABLE chart_workspace_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scope VARCHAR(50) NOT NULL,
name VARCHAR(120) NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
layout_mode VARCHAR(50) NOT NULL,
charts_json TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
INSERT INTO chart_workspace_new (
id,
scope,
name,
sort_order,
is_default,
layout_mode,
charts_json,
created_at,
updated_at
)
SELECT
id,
scope,
name,
sort_order,
is_default,
layout_mode,
charts_json,
created_at,
updated_at
FROM chart_workspace;
DROP TABLE chart_workspace;
ALTER TABLE chart_workspace_new
RENAME TO chart_workspace;
CREATE INDEX idx_chart_workspace_scope_sort
ON chart_workspace (scope, sort_order, id);
CREATE UNIQUE INDEX idx_chart_workspace_scope_default
ON chart_workspace (scope)
WHERE is_default = TRUE;