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)
188 lines
5.2 KiB
Markdown
188 lines
5.2 KiB
Markdown
# 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)
|
|
```python
|
|
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)
|
|
```python
|
|
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)
|
|
```python
|
|
class DatasetsResponse(BaseModel):
|
|
datasets: list[DatasetItem]
|
|
stats: StatsCounts # NEW
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
```
|
|
|
|
### DatasetDetailResponse (modified)
|
|
```python
|
|
class DatasetDetailResponse(BaseModel):
|
|
# ... existing fields unchanged ...
|
|
metrics: list[MetricItem] # NEW
|
|
metric_count: int # NEW
|
|
```
|
|
|
|
### ColumnDescriptionUpdate (new)
|
|
```python
|
|
class ColumnDescriptionUpdate(BaseModel):
|
|
description: str
|
|
```
|
|
|
|
### MetricDescriptionUpdate (new)
|
|
```python
|
|
class MetricDescriptionUpdate(BaseModel):
|
|
description: str
|
|
```
|
|
|
|
---
|
|
|
|
## Frontend Types (TypeScript / Svelte inferred)
|
|
|
|
```typescript
|
|
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
|
|
|
|
```json
|
|
{
|
|
"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
|
|
```
|