Files
ss-tools/specs/030-dataset-lifecycle-workspace/data-model.md
busya b529e8082a feat(030): Dataset Lifecycle Workspace — Stats Bar, split-view, inline-edit, bulk actions
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)
2026-05-21 13:58:47 +03:00

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