Всем привет! Меня зовут Александр Цай, я ведущий инженер-аналитик в МТС Web Services.
Занимаюсь всем, что связано с данными и ИИ. Найти, заполучить, обработать, спроектировать, развернуть — это все ко мне.

Опущу все нудные подробности, решили мы развернуть себе Superset 6.1.0 — последнюю версию, но столкнулись с тем, что заказчику категорически не нравится штатный фильтр по датам и он не хочет пересаживаться со своего BI, а хочет он календарик. А еще — пару плагинов с оглядкой на PowerBI. Поэтому встал вопрос создания собственного плагина-фильтра.

Казалось бы, задача несложная. Пара-тройка гайдов в en сегменте имеется, есть даже документация и штатный генератор шаблонов. Но по факту оказалось, что все это не работает. Генератор так вообще не обновлялся аж с 2024 года: выдает шаблон, который ссылается на уже несуществующие классы и типы, что автоматически делает все гайды неактуальными…

А что все это значит? Время написать свой собственный!
Об этом и расскажу в сегодняшнем материале. 

Дисклеймер: Я питонист до мозга костей и в тайпскрипте, да и фронте в целом, понимаю не очень много. Простите меня, адепты сего языка программирования, но код самого плагина навайбкожен (хоть и проверен насколько я смог).
В npm не запушил тоже умышленно, цель статьи дать исходники и показать, как можно собрать плагин. Любой желающий может что-то додумать и доработать, собрать или разобрать и так далее.

Где живут плагины и фильтры в Superset

Superset — это классический тандем Flask (бэкенд) + React/TypeScript (фронтенд), и вся плагинная кухня находится в папке superset-frontend/src/.

Штатные фильтры лежат в src/filters/components/. Их там пять:

Папка

Ключ

Что делает

Select

filter_select

выпадающий список

Range

filter_range

диапазон значений

Time

filter_time

фильтр по времени

TimeColumn

filter_timecolumn

выбор временной колонки

TimeGrain

filter_timegrain

гранулярность времени

Ключевая мысль здесь это то, что фильтр — это не отдельная магическая сущность, а обычный ChartPlugin, у которого в метаданных проставлено поведение Behavior.NativeFilter. Оно как раз помогает превратить обычный плагин в «native filter», который уже можно добавить на дашборд.

Список фильтров в редакторе дашборда строится из реестра плагинов. Superset берет getChartMetadataRegistry().items и оставляет только те, у кого в метаданных есть поведение Behavior.NativeFilter. Никакого хардкод-списка фильтров в коде нет, а значит, чтобы появился новый фильтр, достаточно зарегистрировать свой ChartPlugin — и он сам всплывет в «+ Add filter».

В Superset для этого есть штатная точка расширения — src/setup/setupPluginsExtra.ts (пустой файл с комментарием «For individual deployments to add custom overrides»). Регистрируем плагин там:

import CalendarFilterPlugin from '@superset-ui/plugin-filter-calendar';

// For individual deployments to add custom overrides
export default function setupPluginsExtra() {
  new CalendarFilterPlugin().configure({ key: 'filter_calendar' }).register();
}

Чтобы в выборе колонки показывались только даты: правим запись filter_calendar: [GenericDataType.Temporal] в карте FILTER_SUPPORTED_TYPES. После пересборки фронтенда плагин появится в списке фильтров на дашборде.

Что берем за основу

Первое, что мы решили сделать, — открыть и внимательно изучить штатные фильтры. Для календаря ближе всего по смыслу Range и Time, они и стали для нас готовым референсом.

Каждый фильтр — это маленький самодостаточный пакет со стандартной структурой. В нашем репозитории он живет в src/, а при установке раскладывается в superset-frontend/plugins/plugin-filter-calendar/ - точно так же, как официальные plugin-chart-*:

src/
├── index.ts                 # регистрация ChartPlugin
├── CalendarFilterPlugin.tsx # сам React-компонент
├── buildQuery.ts            # построение запроса к БД
├── controlPanel.ts          # панель настроек фильтра
├── transformProps.ts        # преобразование данных в пропсы
├── types.ts                 # TypeScript-типы + локальные копии общих типов фильтров
├── common.ts                # локальные копии FilterPluginStyle / StatusMessage / noOp
└── images/thumbnail.png     # иконка

Плюс в соседях есть готовые переиспользуемые штуки, которые нам пригодятся:

  • UI-кит из @superset-ui/core/components — DatePicker, RangePicker, FormItem. Это обертки над antd, так что сам календарик писать не надо — он уже есть, осталось надеть на него правильную «обвязку»;

  • dayjs — уже в зависимостях Superset, его юзаем для работы с датами.

Но важнее всего — понять механику DataMask. Фильтр не ходит сам в БД и не делает запросов за графики дашборда. Вместо этого он через хук setDataMask передает дашборду команду добавить к SQL нужные условия. Вся коммуникация устроена через объект с тремя полями:

  • extraFormData — сами условия фильтрации (что добавить в SQL);

  • filterState — текущее значение и подпись, которую показывать на дашборде;

  • ownState — состояние, которое надо сохранять и восстанавливать.

Как только фильтр отправляет новую маску, дашборд перезапрашивает все свои графики с учетом новых условий. Красиво и просто.

Что пишем

Разберу по файлам, что и зачем. Не буду углубляться в тайпскриптовские дебри, дам суть. Полные файлы в репозитории.

types.ts

Объявляем типы и дефолтные настройки плагина:

export interface PluginFilterCalendarCustomizeProps {
  defaultValue?: string[] | null;
  enableEmptyFilter?: boolean;   // фильтр обязателен к заполнению
  enableSingleDate?: boolean;    // одиночная дата вместо диапазона
}

export const DEFAULT_FORM_DATA = {
  defaultValue: null,
  enableEmptyFilter: false,
  enableSingleDate: false,
};

index.ts

Регистрируем плагин. Класс наследуется от ChartPlugin, в метаданных указываем имя, описание, иконку и — главное — поведение:

export default class CalendarFilterPlugin extends ChartPlugin {
  constructor() {
    const metadata = new ChartMetadata({
      name: t('Calendar filter'),
      description: t('Calendar date range filter plugin'),
      behaviors: [Behavior.InteractiveChart, Behavior.NativeFilter], // ← вот он фильтр
      enableNoResults: false,
      tags: [t('Experimental')],
      thumbnail,
    });

    super({
      buildQuery,
      controlPanel,
      loadChart: () => import('./CalendarFilterPlugin'),
      metadata,
      transformProps,
    });
  }
}

buildQuery.ts

Тут мы просим Superset сходить в БД и вернуть минимальную и максимальную дату по выбранной колонке. Пригодится, чтобы понимать, какие даты вообще есть в данных:

metrics: [
  {
    aggregate: 'MIN',
    column: { column_name: column, type_generic: GenericDataType.Temporal },
    label: 'min',
  },
  {
    aggregate: 'MAX',
    column: { column_name: column, type_generic: GenericDataType.Temporal },
    label: 'max',
  },
]

controlPanel.ts

Панель настроек фильтра в редакторе дашборда. Тут два блока:

  • Query — выбор колонки с датами (обязательный);

  • UI Configuration — две галочки: «Filter value is required» (enableEmptyFilter) и «Single date» (enableSingleDate).

transformProps.ts

По сути это мостик между данными в Superset и пропсами React-компонента, который достает из chartProps готовые хуки дашборда (setDataMask, setHoveredFilter и так далее), данные запроса, текущее состояние фильтра и отдает все это компоненту.

CalendarFilterPlugin.tsx

Самое интересное — React-компонент на хуках, где ядро -— редьюсер поверх useImmerReducer, который накапливает DataMask:

const [dataMask, dispatchDataMask] = useImmerReducer(reducer, {
  extraFormData: {},
  filterState: {},
  ownState: {},
});

А дальше функция updateDataMask, которая превращает выбранные даты в SQL-условия:

const filters: QueryObjectFilterClause[] = [];
if (dates[0] && dates[1] && dates[0] === dates[1]) {
  filters.push({ col, op: '==', val: dates[0] });                  // одна дата
} else {
  if (dates[0]) filters.push({ col, op: '>=', val: dates[0] });    // интервал
  if (dates[1]) filters.push({ col, op: '<=', val: dates[1] });
}
extraFormData.filters = filters;

То есть выбор диапазона превращается в column >= start AND column <= end, выбор одной даты — в column == date. Ровно как PowerBI Date Slicer.

Изменения маски улетают на дашборд через useEffect с вызовом setDataMask(dataMask).

В JSX есть два варианта: 

  • если включен enableSingleDate, рендерим DatePicker, иначе RangePicker с плейсхолдерами «Start date» / «End date»; 

  • если фильтр обязательный и значение не выбрано, показываем StatusMessage с ошибкой валидации.

Ну и отдельная история с disabledDate. Изначально я хотел ограничивать выбор дат диапазоном MIN/MAX из данных, но календарь при этом открывался на другом месяце, и все даты выглядели задизейбленными, что путало коллег. Поэтому в коде есть закомменченый кусок.

🚀 Как интегрируем в платформу

Плагин ставится как полноценный npm-пакет в superset-frontend/plugins/plugin-filter-calendar/ — та же раскладка, что у официальных plugin-chart-*, плюс три однострочные правки в документированных точках расширения:

1. Пакет — кладем плагин в superset-frontend/plugins/plugin-filter-calendar/. npm workspaces (plugins/*) сам свяжет его в node_modules/@superset-ui/plugin-filter-calendar при npm install.

2. Регистрация в src/setup/setupPluginsExtra.ts (штатный хук расширения) импортируем и регистрируем плагин - так он попадает в реестр и появляется в «+ Add filter».

3. Path alias в superset-frontend/tsconfig.json добавляем @superset-ui/plugin-filter-calendar → ./plugins/plugin-filter-calendar/src, чтобы TS/IDE резолвили пакет как у официальных плагинов.

4. Тип колонки в FILTER_SUPPORTED_TYPES (рядом с filter_timegrain) добавляем filter_calendar: [GenericDataType.Temporal], чтобы в выборе колонки были только даты.

Для удобства я набросал install.sh, который раскладывает пакет и вносит правки с проверкой, что плагин еще не зарегистрирован (повторный запуск не дублирует):

Что делает скрипт

Файл

Устанавливает пакет плагина

superset-frontend/plugins/plugin-filter-calendar/

Регистрирует плагин в хуке расширения

src/setup/setupPluginsExtra.ts

Добавляет path alias

superset-frontend/tsconfig.json

Добавляет filter_calendar в FILTER_SUPPORTED_TYPES

FiltersConfigForm/constants.ts

С ним установка сводится к командам:

git clone https://github.com/Kami-sama322/superset-plugin-filter-calendar.git;

chmod +x superset-plugin-filter-calendar/install.sh;
./superset-plugin-filter-calendar/install.sh ./superset;   # корень репозитория Superset

Дальше пересобираем фронтенд. Надеюсь, к этому времени он у вас уже подготовлен. Если нет, то тут можно посмотреть процесс установки.

cd superset-frontend;
npm install;          # свяжет workspace-пакет и поставит зависимости
npm run dev-server;   # режим разработки

Ну и дальше:

  1. Открываем дашборд → Edit+ Add filter → выбираем Calendar filter;

  2. Указываем Column — колонку с датами из датасета (тип Temporal);

  3. При желании включаем Single date;

  4. Жмем Save — на дашборде появляется календарь;

  5. Выбираем дату или диапазон, после чего все графики моментально отфильтровываются.

Выбор фильтра
Выбор фильтра
Внешний вид календаря
Внешний вид календаря

Быстрый старт в Docker для тех, кто дочитал до конца (или хотя бы долистал)

В корне репозитория лежат Dockerfile и docker-compose.yml — образ Superset с уже встроенным плагином:

docker compose build;    # сборка фронтенда Superset с плагином (долго)
docker compose up -d;    # запуск: инициализация БД, админ admin/admin, примеры
# открыть
http://localhost:8088

Вместо заключения

Итак, что мы имеем в итоге:

  • Календарь как native-фильтр на дашбордах с диапазоном дат и одиночной датой без ограничений на выбор;

  • Фильтрация на уровне SQL: column >= start AND column <= end / column == date;

  • Автоустановка одной командой через install.sh.

Полезные ссылки: готовый плагин в GitHub и оригинальная инструкция по запуску Superset в dev-режиме.

Надеюсь, кому-нибудь это сэкономит вечер, а скорее всего пару или даже тройку.

До встречи в следующих материалах!