fix(ui): scope spatial tools to scene
This commit is contained in:
@@ -175,10 +175,7 @@ class SessionStore:
|
||||
).fetchone()
|
||||
if cursor_row is None:
|
||||
raise SessionNotFoundError("observation session cursor was not found")
|
||||
where = (
|
||||
"WHERE (COALESCE(started_at_utc, ''), session_id) < "
|
||||
"(COALESCE(?, ''), ?)"
|
||||
)
|
||||
where = "WHERE (COALESCE(started_at_utc, ''), session_id) < (COALESCE(?, ''), ?)"
|
||||
parameters.extend((cursor_row["started_at_utc"], cursor_row["session_id"]))
|
||||
parameters.append(limit + 1)
|
||||
rows = connection.execute(
|
||||
@@ -309,8 +306,7 @@ class SessionStore:
|
||||
_validate_identifier(session_id, "session id")
|
||||
with self._connect() as connection:
|
||||
session = connection.execute(
|
||||
"SELECT allowed_root, session_root FROM observation_sessions "
|
||||
"WHERE session_id = ?",
|
||||
"SELECT allowed_root, session_root FROM observation_sessions WHERE session_id = ?",
|
||||
(session_id,),
|
||||
).fetchone()
|
||||
if session is None:
|
||||
@@ -372,8 +368,8 @@ class SessionStore:
|
||||
layout: dict[str, Any],
|
||||
) -> WorkspaceLayout:
|
||||
_validate_identifier(workspace_id, "workspace id")
|
||||
if schema_version != 1:
|
||||
raise ValueError("only workspace layout schema version 1 is supported")
|
||||
if schema_version not in {1, 2}:
|
||||
raise ValueError("workspace layout schema version is unsupported")
|
||||
if expected_revision < 0:
|
||||
raise ValueError("expected revision must be non-negative")
|
||||
normalized_name = name.strip()
|
||||
@@ -423,9 +419,9 @@ class SessionStore:
|
||||
def _initialize(self) -> None:
|
||||
with self._connect() as connection:
|
||||
connection.executescript(SCHEMA_SQL)
|
||||
self._migrate_transient_tool_windows(connection)
|
||||
session_columns = {
|
||||
row["name"]
|
||||
for row in connection.execute("PRAGMA table_info(observation_sessions)")
|
||||
row["name"] for row in connection.execute("PRAGMA table_info(observation_sessions)")
|
||||
}
|
||||
for name, declaration in (
|
||||
("plugin_id", "TEXT NOT NULL DEFAULT ''"),
|
||||
@@ -441,9 +437,7 @@ class SessionStore:
|
||||
)
|
||||
artifact_columns = {
|
||||
row["name"]
|
||||
for row in connection.execute(
|
||||
"PRAGMA table_info(observation_session_artifacts)"
|
||||
)
|
||||
for row in connection.execute("PRAGMA table_info(observation_session_artifacts)")
|
||||
}
|
||||
if "replay_byte_length" not in artifact_columns:
|
||||
connection.execute(
|
||||
@@ -454,6 +448,29 @@ class SessionStore:
|
||||
with _ignore_os_error():
|
||||
self.database_path.chmod(0o600)
|
||||
|
||||
def _migrate_transient_tool_windows(self, connection: sqlite3.Connection) -> None:
|
||||
"""Remove v1 scene-tool visibility that never belonged in a layout."""
|
||||
|
||||
rows = connection.execute(
|
||||
"SELECT workspace_id, layout_json FROM workspace_layouts "
|
||||
"WHERE workspace_id = ? AND layout_schema_version = 1",
|
||||
("observation.spatial",),
|
||||
).fetchall()
|
||||
for row in rows:
|
||||
try:
|
||||
layout = json.loads(row["layout_json"])
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
continue
|
||||
if not isinstance(layout, dict) or "tool_windows" not in layout:
|
||||
continue
|
||||
layout.pop("tool_windows", None)
|
||||
connection.execute(
|
||||
"UPDATE workspace_layouts "
|
||||
"SET layout_schema_version = 2, layout_json = ? "
|
||||
"WHERE workspace_id = ? AND layout_schema_version = 1",
|
||||
(_serialize_layout(layout), row["workspace_id"]),
|
||||
)
|
||||
|
||||
def _upsert_candidate(
|
||||
self,
|
||||
source: ObservationArchiveSource,
|
||||
|
||||
@@ -118,7 +118,7 @@ class SceneSettingsDocument(StrictApiModel):
|
||||
show_camera_frustums: bool
|
||||
|
||||
|
||||
class ToolWindowsDocument(StrictApiModel):
|
||||
class LegacyToolWindowsDocument(StrictApiModel):
|
||||
sources_open: bool
|
||||
display_open: bool
|
||||
layers_open: bool
|
||||
@@ -151,12 +151,23 @@ class ViewportSize(StrictApiModel):
|
||||
height: float = Field(gt=0.0, le=100_000.0)
|
||||
|
||||
|
||||
class LayoutPutRequest(StrictApiModel):
|
||||
class LegacyLayoutV1Document(StrictApiModel):
|
||||
version: Literal[1]
|
||||
revision: int = Field(ge=0, le=MAX_SAFE_INTEGER)
|
||||
workspace_id: Literal["observation.spatial"]
|
||||
scene_settings: SceneSettingsDocument
|
||||
tool_windows: ToolWindowsDocument
|
||||
tool_windows: LegacyToolWindowsDocument
|
||||
visible_source_ids: list[str]
|
||||
active_floating_source_id: str | None
|
||||
window_rects: dict[str, NormalizedWindowRect]
|
||||
viewport_size: ViewportSize
|
||||
|
||||
|
||||
class LayoutPutRequest(StrictApiModel):
|
||||
version: Literal[2]
|
||||
revision: int = Field(ge=0, le=MAX_SAFE_INTEGER)
|
||||
workspace_id: Literal["observation.spatial"]
|
||||
scene_settings: SceneSettingsDocument
|
||||
visible_source_ids: list[str]
|
||||
active_floating_source_id: str | None
|
||||
window_rects: dict[str, NormalizedWindowRect]
|
||||
@@ -791,7 +802,12 @@ def build_session_router(
|
||||
def get_workspace_layout(workspace_id: str, response: Response) -> dict[str, Any]:
|
||||
try:
|
||||
stored = store.get_layout(workspace_id)
|
||||
document = _layout_document(stored.workspace_id, stored.revision, stored.layout)
|
||||
document = _layout_document(
|
||||
stored.workspace_id,
|
||||
stored.schema_version,
|
||||
stored.revision,
|
||||
stored.layout,
|
||||
)
|
||||
response.headers["ETag"] = f'"{stored.revision}"'
|
||||
return document
|
||||
except SessionNotFoundError as exc:
|
||||
@@ -824,7 +840,12 @@ def build_session_router(
|
||||
layout=payload,
|
||||
)
|
||||
response.headers["ETag"] = f'"{stored.revision}"'
|
||||
return _layout_document(stored.workspace_id, stored.revision, stored.layout)
|
||||
return _layout_document(
|
||||
stored.workspace_id,
|
||||
stored.schema_version,
|
||||
stored.revision,
|
||||
stored.layout,
|
||||
)
|
||||
except LayoutConflictError as exc:
|
||||
raise HTTPException(status_code=412, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
@@ -885,20 +906,42 @@ def _parse_if_match(value: str) -> int:
|
||||
|
||||
def _layout_document(
|
||||
workspace_id: str,
|
||||
schema_version: int,
|
||||
revision: int,
|
||||
layout: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
if schema_version == 1:
|
||||
try:
|
||||
legacy = LegacyLayoutV1Document.model_validate(
|
||||
{
|
||||
"version": 1,
|
||||
"revision": revision,
|
||||
"workspace_id": workspace_id,
|
||||
**layout,
|
||||
}
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise SessionIntegrityError(
|
||||
"stored workspace layout violates schema version 1"
|
||||
) from exc
|
||||
layout = legacy.model_dump(
|
||||
mode="json",
|
||||
exclude={"version", "revision", "workspace_id", "tool_windows"},
|
||||
)
|
||||
schema_version = 2
|
||||
if schema_version != 2:
|
||||
raise SessionIntegrityError("stored workspace layout schema is unsupported")
|
||||
try:
|
||||
document = LayoutPutRequest.model_validate(
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"revision": revision,
|
||||
"workspace_id": workspace_id,
|
||||
**layout,
|
||||
}
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise SessionIntegrityError("stored workspace layout violates schema version 1") from exc
|
||||
raise SessionIntegrityError("stored workspace layout violates schema version 2") from exc
|
||||
return document.model_dump(mode="json")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user