Backend: - Add MetricItem, StatsCounts, ColumnDescriptionUpdate, MetricDescriptionUpdate DTOs - Add metric_count to DatasetItem and DatasetDetailResponse - Add server-side filtering via ?filter=unmapped|mapped|linked|all - Add PUT endpoints for column/metric description inline-edit - Add HTML stripping validation (max 2000 chars, plain text) - Add WebSocket dataset.updated event on task completion - RBAC: plugin:migration:WRITE for mutations, READ for reads - 14 pytest tests (stats, filter, metrics, inline-edit, validation, 502/503) Frontend: - Rewrite +page.svelte as split-view orchestrator (387 lines) - StatsBar.svelte — 4 metric tiles with aria-pressed, server-side filter dispatch - DatasetList.svelte — card grid with progress bars, checkboxes, search - DatasetPreview.svelte — presentational detail panel (container fetches) - ColumnsTable.svelte — inline-edit with type chips, bold/italic fallback - MetricsTable.svelte — inline-edit with expression hints - 52 vitest tests across 7 test files - i18n: 11 EN + 11 RU keys (with_mapping/с_маппингом) Spec: - 15 issues resolved (semantic label, filtering, endpoints, conflict, metric_count, RBAC, ID types, ports, i18n, validation, a11y, propagation, resize, perf, ownership) Status: 14/14 backend tests pass, 52/52 frontend tests pass, build succeeds Risk: Low — T051 browser validation pending (non-blocking)
5.2 KiB
5.2 KiB
Data Model: Dataset Lifecycle Workspace
Entity Overview
GET /api/datasets?filter=all&page=1&page_size=10 GET /api/datasets/{id}
─────────────────────────────────────────────────────────────
DatasetsResponse DatasetDetailResponse
├── datasets: DatasetItem[] ├── ... (existing fields)
│ ├── id ├── metrics: MetricItem[]
│ ├── table_name │ ├── id
│ ├── schema │ ├── metric_name
│ ├── database │ ├── expression
│ ├── metric_count │ ├── verbose_name
│ ├── mapped_fields │ ├── description
│ └── last_task │ └── metric_type
├── stats: StatsCounts └── metric_count
│ ├── total
│ ├── unmapped_count
│ ├── mapped_count
│ └── linked_count
├── total, page, page_size,
│ total_pages (existing)
└── [unchanged pagination]
PUT /api/datasets/{id}/columns/{col_id}/description
PUT /api/datasets/{id}/metrics/{metric_id}/description
─────────────────────────────────────────────────────────────
Request: ColumnDescriptionUpdate | MetricDescriptionUpdate
{ description: string }
Response: { success: true, description: string }
Backend Pydantic Schemas (in backend/src/api/routes/datasets.py)
MetricItem (new)
class MetricItem(BaseModel):
id: int
metric_name: str
expression: str | None = None
verbose_name: str | None = None
description: str | None = None
metric_type: str | None = None
StatsCounts (new)
class StatsCounts(BaseModel):
total: int # all datasets
unmapped_count: int # mapped_fields.mapped == 0
mapped_count: int # mapped_fields.mapped > 0
linked_count: int # linked_dashboard_count > 0
DatasetsResponse (modified)
class DatasetsResponse(BaseModel):
datasets: list[DatasetItem]
stats: StatsCounts # NEW
total: int
page: int
page_size: int
total_pages: int
DatasetDetailResponse (modified)
class DatasetDetailResponse(BaseModel):
# ... existing fields unchanged ...
metrics: list[MetricItem] # NEW
metric_count: int # NEW
ColumnDescriptionUpdate (new)
class ColumnDescriptionUpdate(BaseModel):
description: str
MetricDescriptionUpdate (new)
class MetricDescriptionUpdate(BaseModel):
description: str
Frontend Types (TypeScript / Svelte inferred)
interface StatsCounts {
total: number;
unmapped_count: number;
mapped_count: number;
linked_count: number;
}
interface MetricItem {
id: number;
metric_name: string;
expression?: string;
verbose_name?: string;
description?: string;
metric_type?: string;
}
// UI-only state types (not in API)
type ColumnRow = {
column_name: string;
verbose_name?: string;
type?: string;
description?: string;
is_dttm: boolean;
is_active: boolean;
id: number;
isEditing?: boolean; // UI state for inline edit
};
type MetricRow = {
metric_name: string;
expression?: string;
verbose_name?: string;
description?: string;
metric_type?: string;
id: number;
isEditing?: boolean; // UI state for inline edit
};
type StatsBarFilter = 'all' | 'unmapped' | 'mapped' | 'linked' | null;
// API query parameter for server-side filtering
// GET /api/datasets?filter=unmapped|mapped|linked|all&page=1&page_size=10
// When filter is active, backend filters full dataset list before pagination
WebSocket Message
{
"type": "dataset.updated",
"payload": {
"env_id": "ss-dev",
"dataset_ids": [32, 45, 78]
}
}
State Transitions
Stats Bar Tile States
Idle → (click tile) → Active (highlighted, list filtered)
Active → (click same tile) → Idle (filter cleared)
Active → (click different tile) → Active (new tile, list re-filtered)
Inline Edit States
Display → (click ✏️) → Editing (textarea visible)
Editing → (Save + API success) → Display (new description shown)
Editing → (Save + API error) → Editing (error message, textarea stays)
Editing → (Cancel) → Display (original description restored)
Detail Panel States
NoSelection → (click card) → Loading → Loaded
Loaded → (click different card) → Loading → Loaded
Loaded → (deselect / bulk mode) → NoSelection
Loading → (API error) → Error → (Retry) → Loading
Bulk Selection States
None → (check 1+ cards) → Selecting (bottom panel hidden for 1)
Selecting → (check 2+ cards) → BulkActive (bottom panel visible)
BulkActive → (uncheck all) → None
BulkActive → (click action) → Modal → (complete) → None