Пишем свой Native Filter плагин для Apache Superset: пошаговый рабочий туториал

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Всем привет! Меня зовут Александр Цай, я ведущий инженер-аналитик в МТС 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 из данных, но календарь при этом открывался на другом месяце, и все даты выглядели задизейбленными, что путало коллег. Поэтому в коде есть закомменченый кусок.

Источник: https://habr.com/ru/companies/ru_mts/articles/1067600/


Интересные статьи

Интересные статьи

Многие читатели Хабра успели полюбить телефоны BlackBerry — как относительно старые телефоны, так и смартфоны последних лет, разработанные уже после продажи бизнеса компании другому бренду. Автор ...
Данной статьей я начинаю цикл статей по интеграции Битрикс24, а именно разборов кейсов работы обычных компаний и подбора решений, с помощью которых можно значительно упростить жизнь этих компаний. Сле...
Эта статья для тех, кто собирается открыть интернет-магазин, но еще рассматривает варианты и думает по какому пути пойти, заказать разработку магазина в студии, у фрилансера или выбрать облачный серви...
Мы разрабатываем платформу для визуальной коллаборации. Для отображения контента мы используем Canvas: на нём рисуется всё, в том числе тексты. Готового решения для отображения текстов на Canvas ...
Основанная в 1998 году компания «Битрикс» заявила о себе в 2001 году, запустив первый в России интернет-магазин программного обеспечения Softkey.ru.