feat: enhance table layout and interaction across various components
Deploy production / deploy (push) Successful in 53s

- Introduced a new `WorkspaceTable` component for consistent table styling and behavior.
- Updated `BacktestPage`, `DatasetPage`, and `DataPreparationPage` to utilize the new `WorkspaceTable`.
- Implemented compact row heights and ellipsis for long text in table cells.
- Added validation for online field queries to ensure at least one filter is provided.
- Enhanced error handling and user feedback for invalid input scenarios.
- Improved pagination controls with dynamic page size options.
- Documented table display specifications in `ui-tables.md`.
- Added tests to verify table layout, interaction, and error handling.
This commit is contained in:
yuxuanhui
2026-09-12 02:13:48 +08:00
parent d9fbaa7cf7
commit b9975e58e9
19 changed files with 586 additions and 81 deletions
+12 -3
View File
@@ -4,7 +4,7 @@ import hashlib
import json
from typing import Literal
from pydantic import Field, field_validator, model_validator
from pydantic import ConfigDict, Field, field_validator, model_validator
from ..preparations.contracts import PreparationReference
from ..schemas import Contract
@@ -56,6 +56,15 @@ class Source(Contract):
hypothesis: str | None = Field(default=None, max_length=2000)
class SourceOutput(Source):
"""Project stored metadata onto the public source fields; input remains strict.
Removed or internal keys are never returned or converted to snapshot references.
Known fields still undergo normal validation.
"""
model_config = ConfigDict(extra="ignore")
class DraftInput(Contract):
@@ -145,7 +154,7 @@ class PreviewOutput(Contract):
preview_id: str
version: int
name: str
source: Source
source: SourceOutput
digest: str
total: int
batch_count: int
@@ -163,7 +172,7 @@ class RunOutput(Contract):
backtest_run_id: str
preview_id: str
name: str
source: Source
source: SourceOutput
ai_context: dict
control: Literal["active", "paused", "stopped"]
status: str
+10
View File
@@ -136,6 +136,16 @@ class Preparations:
)
async def online_fields(self, filters):
text_filters = (filters.q, filters.dataset_id, filters.field_type)
numeric_filters = (
filters.coverage_min, filters.coverage_max,
filters.user_count_min, filters.user_count_max,
filters.alpha_count_min, filters.alpha_count_max,
)
if not any(value and value.strip() for value in text_filters) and not any(
value is not None for value in numeric_filters
):
raise HTTPException(422, "在线查询至少需要关键词、数据集、类型或数值筛选条件")
if self.client is None:
raise HTTPException(409, "请先连接 WorldQuant")
params = dict(
+27
View File
@@ -411,3 +411,30 @@ async def test_terminal_detail_failure_releases_slot_but_keeps_platform_success(
await tick(lane)
assert len(platform.posts) == 2
await lane.interrupt()
async def test_run_list_projects_source_metadata_without_accepting_unknown_input(app, logged_in):
"""Persisted source metadata must not turn the entire run list into a 500."""
await setup(app)
created = await start(logged_in, await preview(logged_in))
async with app.state.sessions.begin() as db:
row = await db.get(BacktestRun, created["backtest_run_id"])
row.source = {**row.source, "template_input_id": None, "extra_metadata": "not-an-input"}
response = await logged_in.get(f"{PREFIX}/runs?offset=0&source=")
assert response.status_code == 200
source = response.json()["items"][0]["source"]
assert source["kind"] == "test"
assert "template_input_id" not in source and "extra_metadata" not in source
assert source["input_snapshot_ids"] == []
detail = await logged_in.get(f"{PREFIX}/runs/{created['backtest_run_id']}")
assert detail.status_code == 200
assert detail.json()["source"] == source
rejected = await logged_in.post(
f"{PREFIX}/drafts",
json={
"name": "invalid",
"source": {"kind": "manual", "extra_metadata": "invalid"},
"candidates": [candidate()],
},
)
assert rejected.status_code == 422
+14 -3
View File
@@ -88,12 +88,13 @@ async def test_scope_dimensions_rejected_without_creating_collection(catalog, ke
async def test_online_fields_without_sync_and_local_search_before_pagination(catalog):
client, _, _ = catalog
response = await client.get("/api/v1/catalog/worldquant/fields", params={**SCOPE, "limit": 100})
params = {**SCOPE, "dataset_id": "TEST_FIN", "limit": 100}
response = await client.get("/api/v1/catalog/worldquant/fields", params=params)
assert response.status_code == 200, response.text
first = response.json()
assert len(first["items"]) == 100 and first["has_more"]
second = (
await client.get("/api/v1/catalog/worldquant/fields", params={**SCOPE, "limit": 100, "offset": 100})
await client.get("/api/v1/catalog/worldquant/fields", params={**params, "offset": 100})
).json()
assert len(second["items"]) > 0
assert (await client.get("/api/v1/catalog/fields", params=SCOPE)).json()["total"] == 0
@@ -298,7 +299,8 @@ async def test_full_sync_restart_keeps_page_and_auth_pauses_all_datasets(catalog
async def test_online_instrument_type_is_verified(catalog):
client, _, state = catalog
state["fields"][0]["instrumentType"] = "FUTURE"
assert (await client.get("/api/v1/catalog/worldquant/fields", params=SCOPE)).status_code == 502
response = await client.get("/api/v1/catalog/worldquant/fields", params={**SCOPE, "q": "TEST"})
assert response.status_code == 502
async def test_mcp_collection_reads_and_versioned_submit_contract(catalog):
@@ -350,3 +352,12 @@ async def test_retry_waiting_full_job_requeues_its_checkpoint(catalog):
async with runner.sessions.begin() as db:
result = await Business(db).retry_job(job.id)
assert result["status"] == "queued" and result["checkpoint"]["offset"] == 100
async def test_online_fields_reject_empty_criteria_before_requesting_platform(catalog):
client, _, state = catalog
before = list(state["calls"])
for params in (SCOPE, {**SCOPE, "q": " ", "dataset_id": ""}):
response = await client.get("/api/v1/catalog/worldquant/fields", params=params)
assert response.status_code == 422
assert state["calls"] == before