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)
29 KiB
Feature Specification: Dataset Lifecycle Workspace
Feature Branch: 030-dataset-lifecycle-workspace
Created: 2026-05-19
Status: Draft
Input: Feature 029 — Dataset Lifecycle Workspace (variant C)
Clarifications
Session 2026-05-19
- Q: Stats Bar фильтрует клиентски или серверно? → A: Серверная фильтрация через query параметр. Бэкенд в ответе
GET /api/datasetsвозвращает 4 stats-числа (total,unmapped_count,mapped_count,linked_count). При клике на метрику Stats Bar фронтенд отправляет запрос с?filter=unmapped|mapped|linked|allиfilterсбрасываетpageна 1. Пагинация остаётся серверной; бэкенд фильтрует полный список перед пагинацией. Stats-числа считаются глобально (не зависят от фильтра). - Q: Каким запросом сохранять inline-edit описания колонок/метрик? → A: Два новых ss-tools эндпоинта:
PUT /api/datasets/{id}/columns/{col_id}/descriptionиPUT /api/datasets/{id}/metrics/{metric_id}/description(body:{description: "text"}). Бэкенд выполняет GET датасета из Superset, модифицирует одну колонку/метрику, PUT обратно полный объект. В Superset нет PATCH — только PUT с полным датасетом. - Q: Как обрабатывать конфликт одновременного редактирования? → A: Last-write-wins. Бэкенд всегда выполняет GET→modify→PUT без проверки версии. Последний сохранивший перезаписывает. Для описаний колонок/метрик это приемлемый риск. ETag/блокировки — out of scope.
- Q: Параметр
override_columnsпри PUT в Superset? → A: Всегдаoverride_columns=false. Бэкенд получает полный датасет из GET, модифицирует только description нужной колонки, PUT обратно сoverride_columns=false. Superset сохраняет все колонки, не участвовавшие в payload. - Q: Как Stats Bar узнаёт об изменении counts после асинхронных задач? → A: WebSocket push. При завершении mapping/docs задачи сервер отправляет событие
dataset.updatedчерез существующий WebSocket. Фронтенд по событию перезапрашиваетGET /api/datasetsи обновляет Stats Bar + карточки автоматически.
User Scenarios & Testing
User Story 1 — Stats-Driven Overview & Filtering (Priority: P1)
Пользователь заходит на страницу датасетов и видит 4 ключевые метрики в Stats Bar вверху. Каждая метрика кликабельна как фильтр списка. Метрики дают мгновенный обзор состояния датасетов и позволяют быстро отфильтровать те, которые требуют внимания.
Why this priority: Stats bar — первое, что видит пользователь. Он заменяет текущую пустую область над таблицей и даёт моментальный actionable insight. Без него страница неотличима от текущей реализации.
Independent Test: Перейти на /datasets, проверить что каждая из 4 метрик отображается и клик по ней фильтрует список.
Acceptance Scenarios:
- Given пользователь видит страницу датасетов с загруженными данными, When страница загружена, Then Stats Bar показывает 4 метрики: Все (total), Без маппинга, С маппингом, С дашбордами
- Given Stats Bar отображается, When пользователь кликает на "Без маппинга", Then список фильтруется до датасетов с
mapped_fields.mapped === 0, метрика подсвечена - Given Stats Bar отображается, When пользователь кликает на уже активный фильтр, Then фильтр сбрасывается и показываются все датасеты
- Given API возвращает ошибку при загрузке, When страница рендерится, Then Stats Bar показывает 0 для всех метрик или состояние ошибки
User Story 2 — Split-View с карточками и детальной панелью (Priority: P1)
Пользователь видит split-view: слева (40%) — список датасетов в виде карточек с прогресс-баром маппинга, справа (60%) — детальный просмотр выбранного датасета без перезагрузки страницы. На мобильных (< 768px) список занимает весь экран, а детали открываются как slide-over.
Why this priority: Split-view — ядро workspace. Без него нет смысла в Stats Bar и inline-редактировании. Это главное UX-изменение относительно текущей таблицы + отдельной страницы деталей.
Independent Test: Выбрать датасет в левой панели, убедиться что правая панель показывает его детали без перезагрузки страницы.
Acceptance Scenarios:
- Given страница загружена, When список датасетов отрендерен, Then карточки отображаются в левой панели (40%), правая панель показывает placeholder "Выберите датасет"
- Given список загружен, When пользователь кликает на карточку датасета, Then правая панель загружает детали выбранного датасета (имя, схема, колонки, метрики, дашборды)
- Given датасет выбран в правой панели, When пользователь кликает на другую карточку, Then правая панель обновляется без полной перезагрузки страницы
- Given ширина экрана < 768px, When пользователь кликает на карточку, Then правая панель открывается как slide-over поверх списка
- Given slide-over открыт на мобильном, When пользователь кликает "Назад" или вне slide-over, Then slide-over закрывается, возвращаясь к списку
User Story 3 — Карточки датасетов с прогресс-баром (Priority: P2)
Каждая карточка в левой панели содержит: имя таблицы, схему, количество колонок, количество метрик, прогресс-бар маппинга (mapped/total в процентах), кнопки быстрых действий (Маппинг, Доки). Прогресс-бар визуально показывает состояние маппинга.
Why this priority: Карточки — основной навигационный элемент. Без них split-view бесполезен, но технически карточки зависят от структуры split-view (US2).
Independent Test: Проверить что карточка содержит все обязательные поля и прогресс-бар правильно отражает mapped/total.
Acceptance Scenarios:
- Given список датасетов загружен, When карточки отрендерены, Then каждая карточка показывает: имя таблицы, схему, кол-во колонок, кол-во метрик, прогресс-бар
- Given датасет имеет
mapped_fields: {total: 10, mapped: 7}, When карточка рендерится, Then прогресс-бар заполнен на 70% зелёным/жёлтым/синим в зависимости от процента - Given датасет без метрик (metric_count = 0), When карточка рендерится, Then кол-во метрик показывает "0" или скрыто
- Given карточка отображается, When пользователь нажимает кнопку "Маппинг", Then открывается модалка маппинга колонок для этого датасета
User Story 4 — Детальная панель с колонками и метриками (Priority: P2)
Правая панель показывает: хедер датасета (полное имя, схема, кол-во колонок/метрик, связанные дашборды, дата создания), секцию "Колонки" (таблица с column_name, verbose_name, type, description, кнопка inline-редактирования), секцию "Метрики" (таблица с metric_name, expression, verbose_name, description, кнопка inline-редактирования), связанные дашборды.
Why this priority: Детальная панель — вторая половина split-view. Без неё нет рабочего пространства, но технически зависит от US2.
Independent Test: Выбрать датасет с метриками и колонками, проверить что обе секции отображаются корректно.
Acceptance Scenarios:
- Given датасет выбран, When правая панель загружена, Then хедер показывает:
table_name,schema,column_count,metric_count,linked_dashboard_count,created_on - Given у колонки есть
verbose_name, When секция колонок рендерится, Thenverbose_nameпоказывается жирным,column_nameне показывается - Given у колонки нет
verbose_name, When секция колонок рендерится, Thencolumn_nameпоказывается курсивом как fallback - Given у колонки/метрики нет
description, When строка рендерится, Then показывается "✏️ Добавить описание" как action prompt - Given у метрики нет
description, When строка метрики рендерится, Then под именем показываетсяexpressionкак подсказка
User Story 5 — Inline-редактирование описаний (Priority: P3)
Пользователь может редактировать description колонок и метрик прямо в детальной панели. Клик по ✏️ на строке открывает textarea, пользователь вводит текст, сохраняет через API. Изменения применяются без перезагрузки страницы.
Why this priority: Inline-редактирование — ключевая фича workspace, но она менее критична чем базовая навигация и просмотр. Зависит от US4 (детальная панель) и бэкенд-эндпоинта.
Independent Test: Кликнуть ✏️ на колонке, ввести описание, сохранить — проверить что описание обновилось и отображается.
Acceptance Scenarios:
- Given строка колонки без описания, When пользователь кликает "✏️ Добавить описание", Then появляется textarea с кнопками "Сохранить" и "Отмена"
- Given textarea открыта, When пользователь вводит текст и нажимает "Сохранить", Then описание колонки сохраняется через
PUT /api/datasets/{id}/columns/{col_id}/description, описание метрики — черезPUT /api/datasets/{id}/metrics/{metric_id}/description; textarea закрывается, описание отображается - Given textarea открыта, When пользователь нажимает "Отмена", Then textarea закрывается без изменений
- Given сохранение не удалось (ошибка API), When пользователь нажимает "Сохранить", Then показывается сообщение об ошибке, textarea остаётся открытой для повторной попытки
- Given строка колонки с существующим описанием, When пользователь кликает ✏️, Then textarea открывается с предзаполненным текстом описания
User Story 6 — Bulk-режим с нижней панелью (Priority: P3)
Чекбоксы на карточках позволяют выбрать несколько датасетов. При выборе 2+ датасетов появляется нижняя панель с bulk-действиями: "Маппинг колонок" и "Генерация документации". Модалки bulk-маппинга и генерации доков переиспользуются из текущей реализации.
Why this priority: Bulk-режим уже существует в текущей реализации, его нужно адаптировать под карточки в split-view. Менее приоритетно чем базовый просмотр.
Independent Test: Выбрать 3 датасета через чекбоксы, проверить что нижняя панель появляется и кнопки работают.
Acceptance Scenarios:
- Given карточки отображаются с чекбоксами, When пользователь отмечает 2+ датасета, Then нижняя панель появляется с кнопками "Маппинг колонок" и "Генерация документации"
- Given нижняя панель видна, When пользователь снимает выделение со всех датасетов, Then панель исчезает
- Given нижняя панель видна, When пользователь нажимает "Маппинг колонок", Then открывается модалка маппинга (переиспользованная из текущей реализации)
- Given выбраны датасеты с разных страниц пагинации, When нижняя панель отображается, Then счётчик показывает общее количество выбранных датасетов
User Story 7 — Отображение метрик в API и UI (Priority: P2)
Бэкенд API возвращает метрики в DatasetDetailResponse. Фронтенд отображает их в секции "Метрики" детальной панели.
Why this priority: Метрики — обязательная часть детальной панели (US4), но требуют изменений на бэкенде. Без бэкенд-поддержки UI нечего показывать.
Independent Test: Вызвать GET /api/datasets/{id}, проверить наличие полей metrics и metric_count в ответе, и что MetricItem содержит все обязательные поля.
Acceptance Scenarios:
- Given датасет имеет метрики в Superset API, When бэкенд получает
GET /api/datasets/{id}, Then ответ содержитmetrics: [...]иmetric_count - Given у метрики есть все поля Superset API, When бэкенд трансформирует ответ, Then
MetricItemсодержит:id,metric_name,expression,verbose_name,description,metric_type - Given Superset API возвращает датасет без метрик, When бэкенд обрабатывает ответ, Then
metrics: []иmetric_count: 0
Edge Cases
- Что происходит когда API датасетов возвращает 503 (Superset недоступен)?
- Как обрабатывается случай когда
detailAPI для выбранного датасета падает, но список загружен? - Как ведёт себя split-view при ресайзе окна между десктопным и мобильным layout? → Решено: Выбранный датасет сохраняется. При сжатии до <768px открытая детальная панель становится slide-over (открытым). При расширении до ≥768px восстанавливается split-view; если slide-over был закрыт — правая панель показывает последний выбранный датасет или placeholder. (FR-034–FR-036)
- Что показывать в Stats Bar когда данные ещё не загружены (loading state)?
- Как обрабатывать случай когда у датасета 0 колонок и 0 метрик?
- Что происходит при сохранении inline-edit когда несколько пользователей редактируют один датасет (конфликт)? → Решено: last-write-wins. Последний сохранивший перезаписывает. ETag/блокировки — out of scope.
- Как Stats Bar обновляет числа при переключении окружения — запрашивает новые counts?
Requirements
Functional Requirements
- FR-001: Система MUST отображать Stats Bar с 4 метриками (Все, Без маппинга, С маппингом, С дашбордами) вверху страницы
/datasets - FR-002: Каждая метрика в Stats Bar MUST быть кликабельной и действовать как фильтр списка датасетов. Фильтрация MUST выполняться серверно через query параметр
?filter=unmapped|mapped|linked|allс серверной пагинацией. Stats-числа считаются глобально. - FR-003: Система MUST отображать split-view layout: левая панель (40%) со списком карточек, правая панель (60%) с деталями выбранного датасета
- FR-004: При ширине экрана < 768px правая панель MUST открываться как slide-over поверх списка
- FR-005: Карточка датасета MUST содержать: имя таблицы, схему, количество колонок, количество метрик, прогресс-бар маппинга, кнопки быстрых действий
- FR-006: Прогресс-бар MUST визуально отображать процент маппинга (mapped/total) с цветовой кодировкой: зелёный (100%), жёлтый (≥50%), синий (<50%)
- FR-007: Правая панель MUST показывать хедер датасета (полное имя, схема, кол-во колонок/метрик, связанные дашборды, дата создания)
- FR-008: Правая панель MUST содержать секцию "Колонки" с таблицей (column_name, verbose_name, type, description, ✏️ inline edit)
- FR-009: Правая панель MUST содержать секцию "Метрики" с таблицей (metric_name, expression, verbose_name, description, ✏️ inline edit)
- FR-010: При наличии
verbose_nameсистема MUST показывать его жирным; при отсутствии — показыватьcolumn_name/metric_nameкурсивом - FR-011: При отсутствии
descriptionсистема MUST показывать "✏️ Добавить описание" как action prompt - FR-012: Для метрик без описания система MUST показывать
expressionкак подсказку под именем - FR-013: Inline-редактирование MUST поддерживать открытие textarea с кнопками "Сохранить"/"Отмена", с предзаполнением существующего описания
- FR-014: Сохранение описания колонки MUST выполняться через
PUT /api/datasets/{id}/columns/{col_id}/description(body:{description: "text"}). Сохранение описания метрики —PUT /api/datasets/{id}/metrics/{metric_id}/description. Бэкенд выполняет GET→modify→PUT в Superset. - FR-015: Карточки MUST содержать чекбоксы для bulk-выбора, при 2+ выбранных MUST появляться нижняя панель с bulk-действиями
- FR-016: Bulk-действия "Маппинг колонок" и "Генерация документации" MUST переиспользовать существующие модалки
- FR-017:
DatasetDetailResponseна бэкенде MUST включатьmetrics: list[MetricItem]иmetric_count: int - FR-018:
MetricItemPydantic DTO MUST содержать поля:id,metric_name,expression,verbose_name,description,metric_type - FR-019:
SupersetClient.get_dataset_detailMUST извлекатьdataset.get("metrics", [])из ответа Superset API - FR-020: Система MUST поддерживать i18n ключи (EN/RU) для: all, without_mapping, with_mapping, linked_to_dashboards, metrics, metric_count, no_selection, save_description, add_description
- FR-021: Breadcrumb для
/datasetsMUST отображать "Датасеты / Datasets" (EN/RU), а не "Маппер колонок" - FR-022: Ответ
GET /api/datasetsMUST включать поля stats:total(int),unmapped_count(int),mapped_count(int),linked_count(int) — для отрисовки Stats Bar без дополнительных запросов - FR-023: PUT в Superset при сохранении inline-edit MUST выполняться с
override_columns=false(query parameter) — предотвращает случайное удаление колонок, не упомянутых в payload - FR-024: По WebSocket-событию
dataset.updated(отправляемому при завершении mapping/docs задачи) фронтенд MUST перезапрашиватьGET /api/datasetsи обновлять Stats Bar counts + карточки без ручного «Обновить» - FR-025:
DatasetItemв list response MUST включатьmetric_count: int(количество метрик датасета) для отображения на карточках - FR-026:
GET /api/datasetsMUST поддерживать query параметр?filter=unmapped|mapped|linked|allдля серверной фильтрации перед пагинацией. При отсутствии параметра поведение эквивалентноfilter=all. - FR-027: Stats Bar метрики MUST быть кнопками с
aria-pressed, отражающим активное состояние фильтра - FR-028: Slide-over на мобильных MUST захватывать фокус при открытии и закрываться по Escape
- FR-029: Ошибки сохранения inline-edit MUST анонсироваться через
aria-liveрегион - FR-030: Skeleton-лоадеры MUST использовать
aria-busy="true"на время загрузки - FR-031: Клик по чекбоксу карточки MUST только переключать выделение датасета, НЕ открывая детальную панель
- FR-032: Клик по quick-action кнопке на карточке MUST открывать соответствующую модалку, НЕ открывая детальную панель
- FR-033: Клик по телу карточки (вне чекбокса и quick-action кнопок) MUST выбирать датасет и открывать детальную панель
- FR-034: Выбранный датасет MUST сохраняться при ресайзе между десктопным и мобильным layout без потери состояния
- FR-035: При ресайзе с десктопа на мобильный: если детальная панель открыта, она MUST перейти в режим slide-over (открытый)
- FR-036: При ресайзе с мобильного на десктоп: если slide-over закрыт, правая панель MUST показать placeholder или последний выбранный датасет
- FR-037: Сохранение описания MUST принимать plain text, max 2000 символов. Пустая строка разрешена (очищает описание). Пробельные символы в начале/конце сохраняются без обрезки. 404 при отсутствии датасета/колонки/метрики. 502/503 при недоступности Superset.
Key Entities
- StatsBarMetric: Метрика в Stats Bar — label, count, icon, filter_key, is_active
- DatasetCard: Карточка в левой панели — id, table_name, schema, column_count, metric_count, mapped_progress (%), quick_actions[]
- DatasetDetailPanel: Детальная панель — header_info, columns_table, metrics_table, linked_dashboards
- MetricItem (новый): id, metric_name, expression, verbose_name, description, metric_type
- ColumnRow: Строка в таблице колонок — column_name, verbose_name, type, description, is_editing (UI state)
- MetricRow: Строка в таблице метрик — metric_name, expression, verbose_name, description, is_editing (UI state)
- BulkActionPanel: Нижняя панель — selected_count, available_actions[], is_visible
Success Criteria
Measurable Outcomes
- SC-001: Пользователь может за 1 клик перейти от Stats Bar к отфильтрованному списку датасетов
- SC-002: Loading state для переключения между датасетами в split-view отображается в течение 100ms; контент деталей загружается согласно latency API (после получения ответа детали рендерятся без дополнительной задержки)
- SC-003: Пользователь может отредактировать и сохранить описание колонки/метрики < 3 кликов
- SC-004: Stats Bar корректно обновляет значения при переключении окружения
- SC-005: Все существующие bulk-действия (маппинг, генерация доков) продолжают работать в новом UI
- SC-006: Страница корректно адаптируется между десктопным split-view и мобильным slide-over без потери функциональности
- SC-007: Все новые строки интерфейса имеют переводы на EN и RU