Files
ss-tools/specs/030-dataset-lifecycle-workspace/spec.md
busya ec6421de35 rename ss-tools to superset-tools across the entire project
- Replace all occurrences of 'ss-tools' with 'superset-tools' in 104 files
- Rename git bundle file ss-tools.bundle → superset-tools.bundle
- Update .gitignore pattern accordingly
- Preserve variable names (hasSsTools etc.) and code identifiers
2026-06-16 11:15:19 +03:00

214 lines
29 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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: Два новых superset-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**:
1. **Given** пользователь видит страницу датасетов с загруженными данными, **When** страница загружена, **Then** Stats Bar показывает 4 метрики: Все (total), Без маппинга, С маппингом, С дашбордами
2. **Given** Stats Bar отображается, **When** пользователь кликает на "Без маппинга", **Then** список фильтруется до датасетов с `mapped_fields.mapped === 0`, метрика подсвечена
3. **Given** Stats Bar отображается, **When** пользователь кликает на уже активный фильтр, **Then** фильтр сбрасывается и показываются все датасеты
4. **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**:
1. **Given** страница загружена, **When** список датасетов отрендерен, **Then** карточки отображаются в левой панели (40%), правая панель показывает placeholder "Выберите датасет"
2. **Given** список загружен, **When** пользователь кликает на карточку датасета, **Then** правая панель загружает детали выбранного датасета (имя, схема, колонки, метрики, дашборды)
3. **Given** датасет выбран в правой панели, **When** пользователь кликает на другую карточку, **Then** правая панель обновляется без полной перезагрузки страницы
4. **Given** ширина экрана < 768px, **When** пользователь кликает на карточку, **Then** правая панель открывается как slide-over поверх списка
5. **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**:
1. **Given** список датасетов загружен, **When** карточки отрендерены, **Then** каждая карточка показывает: имя таблицы, схему, кол-во колонок, кол-во метрик, прогресс-бар
2. **Given** датасет имеет `mapped_fields: {total: 10, mapped: 7}`, **When** карточка рендерится, **Then** прогресс-бар заполнен на 70% зелёным/жёлтым/синим в зависимости от процента
3. **Given** датасет без метрик (metric_count = 0), **When** карточка рендерится, **Then** кол-во метрик показывает "0" или скрыто
4. **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**:
1. **Given** датасет выбран, **When** правая панель загружена, **Then** хедер показывает: `table_name`, `schema`, `column_count`, `metric_count`, `linked_dashboard_count`, `created_on`
2. **Given** у колонки есть `verbose_name`, **When** секция колонок рендерится, **Then** `verbose_name` показывается жирным, `column_name` не показывается
3. **Given** у колонки нет `verbose_name`, **When** секция колонок рендерится, **Then** `column_name` показывается курсивом как fallback
4. **Given** у колонки/метрики нет `description`, **When** строка рендерится, **Then** показывается "✏ Добавить описание" как action prompt
5. **Given** у метрики нет `description`, **When** строка метрики рендерится, **Then** под именем показывается `expression` как подсказка
---
### User Story 5 — Inline-редактирование описаний (Priority: P3)
Пользователь может редактировать description колонок и метрик прямо в детальной панели. Клик по на строке открывает textarea, пользователь вводит текст, сохраняет через API. Изменения применяются без перезагрузки страницы.
**Why this priority**: Inline-редактирование ключевая фича workspace, но она менее критична чем базовая навигация и просмотр. Зависит от US4 (детальная панель) и бэкенд-эндпоинта.
**Independent Test**: Кликнуть на колонке, ввести описание, сохранить проверить что описание обновилось и отображается.
**Acceptance Scenarios**:
1. **Given** строка колонки без описания, **When** пользователь кликает "✏ Добавить описание", **Then** появляется textarea с кнопками "Сохранить" и "Отмена"
2. **Given** textarea открыта, **When** пользователь вводит текст и нажимает "Сохранить", **Then** описание колонки сохраняется через `PUT /api/datasets/{id}/columns/{col_id}/description`, описание метрики через `PUT /api/datasets/{id}/metrics/{metric_id}/description`; textarea закрывается, описание отображается
3. **Given** textarea открыта, **When** пользователь нажимает "Отмена", **Then** textarea закрывается без изменений
4. **Given** сохранение не удалось (ошибка API), **When** пользователь нажимает "Сохранить", **Then** показывается сообщение об ошибке, textarea остаётся открытой для повторной попытки
5. **Given** строка колонки с существующим описанием, **When** пользователь кликает , **Then** textarea открывается с предзаполненным текстом описания
---
### User Story 6 — Bulk-режим с нижней панелью (Priority: P3)
Чекбоксы на карточках позволяют выбрать несколько датасетов. При выборе 2+ датасетов появляется нижняя панель с bulk-действиями: "Маппинг колонок" и "Генерация документации". Модалки bulk-маппинга и генерации доков переиспользуются из текущей реализации.
**Why this priority**: Bulk-режим уже существует в текущей реализации, его нужно адаптировать под карточки в split-view. Менее приоритетно чем базовый просмотр.
**Independent Test**: Выбрать 3 датасета через чекбоксы, проверить что нижняя панель появляется и кнопки работают.
**Acceptance Scenarios**:
1. **Given** карточки отображаются с чекбоксами, **When** пользователь отмечает 2+ датасета, **Then** нижняя панель появляется с кнопками "Маппинг колонок" и "Генерация документации"
2. **Given** нижняя панель видна, **When** пользователь снимает выделение со всех датасетов, **Then** панель исчезает
3. **Given** нижняя панель видна, **When** пользователь нажимает "Маппинг колонок", **Then** открывается модалка маппинга (переиспользованная из текущей реализации)
4. **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**:
1. **Given** датасет имеет метрики в Superset API, **When** бэкенд получает `GET /api/datasets/{id}`, **Then** ответ содержит `metrics: [...]` и `metric_count`
2. **Given** у метрики есть все поля Superset API, **When** бэкенд трансформирует ответ, **Then** `MetricItem` содержит: `id`, `metric_name`, `expression`, `verbose_name`, `description`, `metric_type`
3. **Given** Superset API возвращает датасет без метрик, **When** бэкенд обрабатывает ответ, **Then** `metrics: []` и `metric_count: 0`
---
### Edge Cases
- Что происходит когда API датасетов возвращает 503 (Superset недоступен)?
- Как обрабатывается случай когда `detail` API для выбранного датасета падает, но список загружен?
- Как ведёт себя split-view при ресайзе окна между десктопным и мобильным layout? **Решено**: Выбранный датасет сохраняется. При сжатии до <768px открытая детальная панель становится slide-over (открытым). При расширении до 768px восстанавливается split-view; если slide-over был закрыт правая панель показывает последний выбранный датасет или placeholder. (FR-034FR-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`. Бэкенд выполняет GETmodifyPUT в Superset.
- **FR-015**: Карточки MUST содержать чекбоксы для bulk-выбора, при 2+ выбранных MUST появляться нижняя панель с bulk-действиями
- **FR-016**: Bulk-действия "Маппинг колонок" и "Генерация документации" MUST переиспользовать существующие модалки
- **FR-017**: `DatasetDetailResponse` на бэкенде MUST включать `metrics: list[MetricItem]` и `metric_count: int`
- **FR-018**: `MetricItem` Pydantic DTO MUST содержать поля: `id`, `metric_name`, `expression`, `verbose_name`, `description`, `metric_type`
- **FR-019**: `SupersetClient.get_dataset_detail` MUST извлекать `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 для `/datasets` MUST отображать "Датасеты / Datasets" (EN/RU), а не "Маппер колонок"
- **FR-022**: Ответ `GET /api/datasets` MUST включать поля 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/datasets` MUST поддерживать 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