11 KiB
Research: Dataset Lifecycle Workspace (030)
R1 — Backend: Metrics in DatasetDetailResponse
Decision: Add metrics: list[MetricItem] and metric_count: int to DatasetDetailResponse. Add MetricItem Pydantic DTO in backend/src/api/routes/datasets.py. In SupersetClient.get_dataset_detail (backend/src/core/superset_client/_datasets.py), extract dataset.get("metrics", []) from the Superset API response.
Rationale: Superset's GET /api/v1/dataset/{pk} already returns metrics array with metric_name, expression, verbose_name, description, metric_type. Our backend currently ignores them. Extracting them is straightforward — no breaking API change, only additive fields.
Alternatives Considered:
- A separate
/datasets/{id}/metricsendpoint → rejected, adds unnecessary API surface for data already present - Client-side fetch from Superset directly → rejected (bypasses auth, breaks SSR parity)
Impact: DatasetDetailResponse gains 2 new fields. MetricItem DTO ~8 lines. get_dataset_detail gains ~15 lines. Zero DB changes. No migration.
R2 — Backend: Inline-edit Endpoints
Decision: Two new endpoints on ss-tools backend under existing APIRouter(prefix="/api/datasets"):
PUT /api/datasets/{id}/columns/{col_id}/description(body:{"description": "text"})PUT /api/datasets/{id}/metrics/{metric_id}/description(body:{"description": "text"})
Each performs: GET full dataset from Superset via SupersetClient.get_dataset() → modify target column/metric description → PUT back via SupersetClient.update_dataset() with ?override_columns=false.
Rationale: Superset has no PATCH for individual columns/metrics — only PUT on the full dataset with override_columns=false. A convenience endpoint on ss-tools handles the GET→modify→PUT dance server-side, keeping frontend payloads simple ({description: "text"}).
Alternatives Considered:
- Frontend does GET→modify→PUT directly → rejected: requires frontend to handle full Superset payload, increases attack surface
- Single
PUT /api/datasets/{id}with{"columns":[{"id":...,"description":...}]}→ valid but more complex for frontend, reserved for future batch updates - Optimistic save with rollback → rejected: adds complexity without significant UX benefit for description editing
Impact: +2 route handlers in datasets.py (~50 lines each). Reuses existing SupersetClient.get_dataset() and SupersetClient.update_dataset(). RBAC: checked via existing has_permission("plugin:migration","READ") or new permission.
R3 — Backend: Stats Counts in GET /api/datasets
Decision: GET /api/datasets response adds stats object with total, unmapped_count, mapped_count, linked_count. Computed by the existing resource_service.get_datasets_with_status() which already fetches ALL datasets before pagination. Stats are computed from the full (non-paginated) dataset list.
Rationale: The Statistics Bar needs aggregate counts. Since the backend already fetches all datasets to compute mapped_fields enrichment, computing stats is O(n) over the already-loaded data — near-zero cost.
Alternatives Considered:
- Separate
/api/datasets/statsendpoint → rejected, adds unnecessary request - Client-side counts (fetch all, count locally) → rejected, pagination would break counts
Impact: DatasetsResponse DTO gains stats: dict. One additional aggregation loop in the route handler (~5 lines). No new DB queries.
R4 — Backend: WebSocket Event dataset.updated
Decision: After a mapping or documentation task completes, the task manager emits a WebSocket event dataset.updated with payload {env_id, dataset_ids: []}. The frontend subscribes to this event channel and automatically re-fetches GET /api/datasets.
Rationale: Existing WebSocket infrastructure already streams task logs to the task drawer. Extending with a completion event avoids polling overhead. The task_manager already has an on_task_complete hook.
Alternatives Considered:
- Polling every N seconds → rejected (unnecessary network traffic, 33 datasets trivial but doesn't scale)
- Manual refresh only → rejected by user in clarification Q5
Impact: One additional WebSocket message type in task completion handler (~5 lines). Frontend subscription in +page.svelte (~10 lines). No new infrastructure.
R5 — Frontend: Component Architecture
Decision: Replace current monolithic 974-line +page.svelte with composition of 5 leaf components:
+page.svelte (layout orchestrator, ~200 lines)
├── StatsBar.svelte (4 metric tiles, client-side filtering)
├── DatasetList.svelte (card list with pagination, checkbox selection)
├── DatasetPreview.svelte (right panel: header + columns + metrics)
│ ├── ColumnsTable.svelte (column rows with inline-edit ✏️)
│ └── MetricsTable.svelte (metric rows with inline-edit ✏️)
└── BulkActionPanel.svelte (reused from current implementation)
Each component follows ss-tools Svelte 5 conventions (runes $state, $derived, $props, Tailwind classes).
Rationale: SS-tools fractal limit (INV_7): modules <400 lines, contract node cyclomatic complexity <10. The current 974-line page violates both. Decomposition into <400-line components is required by ADR-0002.
Alternatives Considered:
- Keep monolithic + extract inline-edit as helper → rejected (still violates INV_7)
- Use SvelteKit layout groups for sub-panels → rejected (over-engineering; this is one page, not multi-route)
Impact: ~5 new .svelte files in frontend/src/routes/datasets/. Existing [id]/+page.svelte remains as fallback but is largely superseded by DatasetPreview.svelte.
R6 — Frontend: Inline Edit Interaction
Decision: On ✏️ click, the column/metric row enters isEditing state. A <textarea> replaces the description text. Save button calls PUT /api/datasets/{id}/columns/{col_id}/description. On success, row exits editing state and shows new description. On error, textarea stays open with error message. Cancel reverts to original value.
Rationale: Matches standard inline-edit UX pattern. Svelte 5 $state makes isEditing toggle trivial. Optimistic save rejected for simplicity — descriptions are short text, save latency is negligible.
Alternatives Considered:
- Modal with single textarea → rejected (too heavy for one field)
- Click-to-edit with blur-to-save → rejected (ambiguous "cancel" path)
Impact: ColumnsTable.svelte and MetricsTable.svelte each have isEditingRowId state + textarea logic. ~30 lines of template logic per table.
R7 — Frontend: WebSocket Subscription
Decision: +page.svelte subscribes to dataset.updated WebSocket events in onMount. On receiving event with matching env_id, calls loadDatasets() to refresh the list and stats. No visual feedback needed — list updates in-place (Svelte reactivity).
Rationale: Pushes state updates without user action. Existing WebSocket connection is reused (no new connection). Svelte reactivity handles re-render.
Alternatives Considered:
- SSE / EventSource → rejected (WebSocket already connected for task logs)
- Separate WebSocket channel → rejected (overhead)
Impact: ~10 lines in +page.svelte. Reuses wsStore or equivalent from existing WebSocket infrastructure.
R8 — i18n: New Keys
Decision: Add 11 new keys to frontend/src/lib/i18n/locales/{en,ru}/datasets.json: all, without_mapping, with_description, linked_to_dashboards, metrics, metric_count, no_selection, save_description, add_description, columns, detail_empty. No structural changes to i18n system.
Rationale: Existing i18n uses flat JSON key files loaded by locale. Adding keys is additive. No new dependencies.
Impact: +11 lines per locale file. Components reference via $t.datasets.all, etc.
R9 — Backend: override_columns=false Guarantee
Decision: In line-edit endpoint handlers, when calling SupersetClient.update_dataset(), always append ?override_columns=false to the PUT URL. This ensures Superset preserves all columns/metrics not mentioned in the payload.
Rationale: Superset's default is override_columns=false (safe), but explicit parameter ensures no accidental override if default changes. Risk: with override_columns=true, Superset would DELETE any column not present in the PUT payload — catastrophic.
Alternatives Considered:
- Rely on Superset default → rejected (fragile, no explicit guarantee)
Impact: One query parameter addition per PUT call.
R10 — Module Placement & File Map
| Artifact | File Path | Operation |
|---|---|---|
| MetricItem DTO | backend/src/api/routes/datasets.py |
ADD class |
| Stats counts | backend/src/api/routes/datasets.py |
MODIFY DatasetsResponse, get_datasets() |
| Column description endpoint | backend/src/api/routes/datasets.py |
ADD route PUT /columns/{col_id}/description |
| Metric description endpoint | backend/src/api/routes/datasets.py |
ADD route PUT /metrics/{metric_id}/description |
| Metrics extraction | backend/src/core/superset_client/_datasets.py |
MODIFY get_dataset_detail() |
| WebSocket event | backend/src/core/task_manager.py (or equivalent) |
MODIFY on_task_complete |
| StatsBar | frontend/src/routes/datasets/StatsBar.svelte |
NEW |
| DatasetList | frontend/src/routes/datasets/DatasetList.svelte |
NEW |
| DatasetPreview | frontend/src/routes/datasets/DatasetPreview.svelte |
NEW |
| ColumnsTable | frontend/src/routes/datasets/ColumnsTable.svelte |
NEW |
| MetricsTable | frontend/src/routes/datasets/MetricsTable.svelte |
NEW |
| Page orchestrator | frontend/src/routes/datasets/+page.svelte |
REWRITE |
| i18n EN | frontend/src/lib/i18n/locales/en/datasets.json |
ADD keys |
| i18n RU | frontend/src/lib/i18n/locales/ru/datasets.json |
ADD keys |
| Breadcrumb | frontend/src/lib/components/layout/Breadcrumbs.svelte |
MODIFY |
| Sidebar nav | frontend/src/lib/components/layout/sidebarNavigation.js |
VERIFY |
Unresolved / Deferred
- DatasetItem.metric_count in list view: requires resource_service enrichment. Deferred to implementation — can be computed from full dataset list if metrics data available, else defaults to 0 until backend support is added.
- Accessibility (ARIA): Deferred to Polish phase. Components should use semantic HTML and
aria-busy/aria-livewhere applicable. - Large-scale (200+ datasets): Client-side filtering works for 33 datasets. At 200+, Stats Bar counts remain server-computed; only list filtering would need server-side
?filter=parameter. This is a future enhancement.