Перейти к основному содержимому
Версия: Графиня 2025H2

Документация по разработке модулей

Эта документация описывает, как создать модуль для Графини по публичному контракту.

Модуль в текущей реализации нужен для работы на дашборде. Он может включать:

  • frontend-часть с визуальными компонентами;
  • собственный backend или API-слой;
  • произвольную внутреннюю бизнес-логику и собственную модель данных.

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

Готовый пример

В документацию добавлен учебный пример модуля:

В архиве есть:

  • минимальный набор обязательных библиотек;
  • .npmrc для доступа к пакетам @takelage и @grafinya через Nexus;
  • настроенный vite.config.ts для сборки frontend-части модуля;
  • backend stub с GET /api/v1/well-known и POST /api/v1/demo/status;
  • простые компоненты Widget, WidgetEditor, Settings;
  • window[scope].registry;
  • Vitest-тесты, которые проверяют входные props и callback-параметры каждого компонента.

Этот шаблон можно использовать как стартовую точку для собственного модуля.

Что такое модуль

Модуль это внешнее расширение платформы, которое может состоять из frontend- и backend-частей.

Со стороны интеграции Графиня ожидает, что модуль:

  • публикует метаданные по well-known endpoint;
  • отдает frontend-артефакты модуля, в том числе JS bundle и, при наличии, CSS bundle;
  • регистрирует глобальный объект в window[scope];
  • экспортирует registry с компонентами, которые Графиня может загрузить по имени.

При этом backend модуля:

  • может существовать как отдельный сервис;
  • может иметь произвольный API;
  • может вызываться напрямую из браузера или через proxy backend-а Графини.

Сейчас поддерживается один тип модуля:

  • модуль виджета дашборда.

Такой модуль может содержать до трех frontend-компонентов:

  • Settings — настройки самого модуля;
  • WidgetEditor — редактор виджета модуля;
  • Widget — отображение виджета на дашборде.

Компоненты Settings и WidgetEditor опциональны. Компонент Widget обязателен для модуля, который должен отображаться на дашборде.

Общий порядок подключения

  1. Разработчик публикует модуль по своему URL.
  2. Графиня запрашивает GET {baseUrl}/api/v1/well-known.
  3. Из ответа well-known Графиня получает метаданные модуля: name, title, description, version, entryPoint, frontendHost, scope, components и другие поля.
  4. После сохранения модуля в системе frontend Графини загружает JS по адресу new URL(entryPoint, frontendHost).
  5. Если рядом с JS существует CSS с тем же именем файла, Графиня попытается подключить и его.
  6. После загрузки JS в window[scope] должен появиться объект модуля.
  7. Из этого объекта Графиня берет registry и загружает нужный компонент по имени.

Обязательные frontend-зависимости

Для совместимости с текущим frontend-хостом используйте версии не ниже тех, которые сейчас применяются в Графине:

  • react не ниже 18.3.1
  • react-dom не ниже 18.2.0
  • @takelage/ui не ниже 1.17.1
  • @grafinya/frontend-shared не ниже 1.3.53

Рекомендуется использовать также те же библиотеки, что уже присутствуют в runtime-хосте:

  • react-hook-form
  • @dnd-kit/core
  • @dnd-kit/sortable
  • @dnd-kit/utilities

Доступ к пакетам через Nexus

Для установки @takelage/ui и @grafinya/frontend-shared настройте .npmrc.

Пример:

registry=https://registry.npmjs.org/
strict-ssl=false

@takelage:registry=https://registry.pult.chislitellab.ru/repository/graphina-npm-release/
@grafinya:registry=https://registry.pult.chislitellab.ru/repository/graphina-dev-npm/

; npm 11 показывает предупреждение, если "always-auth" задан в project-level .npmrc.
; Если вашему Nexus нужна предварительная отправка авторизации, добавьте следующую строку в пользовательский ~/.npmrc:
; always-auth=true

//registry.pult.chislitellab.ru/repository/graphina-dev-npm/:username=<REPLACE_WITH_NEXUS_USERNAME>
//registry.pult.chislitellab.ru/repository/graphina-dev-npm/:_password=<REPLACE_WITH_BASE64_PASSWORD>
//registry.pult.chislitellab.ru/repository/graphina-dev-npm/:email=<REPLACE_WITH_NEXUS_EMAIL>

//registry.pult.chislitellab.ru/repository/graphina-npm-release/:username=<REPLACE_WITH_NEXUS_USERNAME>
//registry.pult.chislitellab.ru/repository/graphina-npm-release/:_password=<REPLACE_WITH_BASE64_PASSWORD>
//registry.pult.chislitellab.ru/repository/graphina-npm-release/:email=<REPLACE_WITH_NEXUS_EMAIL>

Замените значения в угловых скобках на реальные учетные данные. Обратите внимание: _password должен содержать корректное значение в base64.

Файл .npmrc уже входит в архив шаблона.

Зачем нужны эти зависимости

  • react и react-dom нужны для совместимого рендеринга компонентов внутри хоста.
  • @takelage/ui нужен для единообразных UI-компонентов, сеток, кнопок, форм и общей визуальной совместимости с приложением.
  • @grafinya/frontend-shared нужен для общих типов, утилит, локализации, форменных и инфраструктурных компонентов, которые уже используются в хост-приложении.
  • react-hook-form нужен, если модуль реализует свои формы по той же модели, что и основной интерфейс.
  • @dnd-kit/* стоит использовать, если редактор модуля или сложные настройки требуют drag-and-drop и должны быть совместимы с текущим окружением.

Runtime-библиотеки, уже доступные в хосте

В странице хоста уже доступны следующие глобальные объекты.

Глобальный объектNPM пакетВерсия в хостеЗачем нужен
window.Reactreact18.3.1Базовый React runtime для всех компонентов модуля
window.ReactDOMreact-dom18.2.xРендеринг React-компонентов и совместимость с DOM API
window.TakelageUI@takelage/ui1.17.1UI-компоненты, формы, кнопки, layout и общий визуальный стиль
window.FrontendShared@grafinya/frontend-shared1.3.53Общие типы, инфраструктурные компоненты, локализация и утилиты платформы
window.ReactHookFormreact-hook-form7.54.2Работа с формами и контроллерами полей
window.DnDKitCore@dnd-kit/core6.3.1Базовые drag-and-drop primitives
window.DnDKitSortable@dnd-kit/sortable10.0.0Сортировка элементов drag-and-drop
window.DnDKitUtilities@dnd-kit/utilities3.2.2Вспомогательные утилиты для drag-and-drop

Если вы собираете модуль как внешний bundle с external-зависимостями, используйте именно эти глобальные имена.

к сведению

Обратите внимание на соответствие имени пакета и имени глобальной переменной:

  • пакет @takelage/ui доступен как window.TakelageUI;
  • пакет @grafinya/frontend-shared доступен как window.FrontendShared.

Требования к frontend-сборке

Рекомендуется использовать:

  • React 18;
  • TypeScript;
  • современные сборщики уровня Vite, Rollup или Webpack;
  • отдельный JS bundle для entry point модуля;
  • отдельный CSS bundle, если модуль использует собственные стили.

Графиня загружает модуль как обычный script, а не как ESM import в runtime. Поэтому после выполнения script в глобальной области должен появиться объект window[scope].

Контракт well-known

Графиня ожидает, что модуль публикует endpoint:

  • GET /api/v1/well-known

Этот endpoint вызывается относительно baseUrl, указанного при подключении модуля.

Минимально необходимые поля

  • name
  • title
  • description
  • version
  • entryPoint
  • frontendHost
  • scope
  • components

На текущий момент критически важны как минимум:

  • name
  • components
  • entryPoint

Но для корректного runtime-подключения рекомендуется всегда возвращать полный набор метаданных.

Пример ответа well-known

{
"name": "custom-dashboard-module",
"title": [
{ "lang": "ru-RU", "value": "Пользовательский модуль" },
{ "lang": "en-US", "value": "Custom module" }
],
"description": [
{ "lang": "ru-RU", "value": "Модуль для пользовательских виджетов" },
{ "lang": "en-US", "value": "Module for custom widgets" }
],
"version": "1.0.0",
"entryPoint": "/assets/remoteEntry.js",
"frontendHost": "https://module.example.com",
"scope": "customDashboardModule",
"components": ["Widget", "WidgetEditor", "Settings"],
"canAddToDashboard": true
}

Назначение полей

  • name — уникальное техническое имя модуля.
  • title — локализованное отображаемое имя.
  • description — локализованное описание модуля.
  • version — версия модуля.
  • entryPoint — путь к JS bundle, который нужно загрузить.
  • frontendHost — базовый host для frontend-артефактов модуля.
  • scope — имя глобального объекта, под которым модуль регистрируется в window.
  • components — список доступных компонентов в registry.
  • canAddToDashboard — можно ли использовать модуль как источник виджета на дашборде.

Контракт JS и CSS артефактов

JS

Frontend Графини строит URL модуля как:

  • new URL(entryPoint, frontendHost)

К URL JS могут быть автоматически добавлены query-параметры:

  • version
  • buildTimestamp

Пример итогового URL:

  • https://module.example.com/assets/remoteEntry.js?version=1.0.0&buildTimestamp=1710000000

CSS

Если entryPoint заканчивается на .js, Графиня попробует загрузить CSS по тому же пути, заменив расширение:

  • /assets/remoteEntry.js -> /assets/remoteEntry.css

Если CSS отсутствует, загрузка модуля не считается ошибкой.

Контракт глобального объекта и registry

После загрузки JS в window[scope] должен появиться объект модуля.

Этот объект должен содержать поле:

  • registry

registry это объект, где ключом является имя frontend-компонента, а значением функция-лоадер.

Пример структуры

window.customDashboardModule = {
registry: {
Widget: async () => ({ default: Widget }),
WidgetEditor: async () => ({ default: WidgetEditor }),
Settings: async () => ({ default: Settings })
}
};

Допустим и вариант, когда loader возвращает сам компонент, но рекомендуемый формат это объект с default.

Frontend-компоненты модуля

Widget

Назначение:

  • отображает пользовательский виджет на дашборде;
  • получает состояние, размеры, временной диапазон, переменные и данные модуля;
  • полностью отвечает за свой UI и собственное поведение.

Статус:

  • обязателен для модуля, который должен работать на дашборде.

Входные параметры компонента Widget

Компонент Widget получает объект props со следующими полями:

ПолеТипНазначение
moduleIdstringИдентификатор модуля
moduleDataRecord<string, unknown>Данные виджета, сохраненные модулем
palettestring[]Текущая палитра цветов
idstringИдентификатор виджета
variablesVariableValues | nullЗначения переменных дашборда
fromstring | undefinedЛевая граница временного диапазона
tostring | undefinedПравая граница временного диапазона
titlestring | undefinedЗаголовок виджета
colsnumber | undefinedШирина виджета в колонках
rowsnumber | undefinedВысота виджета в строках
isDashboardEditbooleanПризнак режима редактирования дашборда
isWidgetEditbooleanПризнак режима редактирования самого виджета
isRefreshClickedbooleanПризнак ручного обновления
setRefreshClicked((value: boolean) => void) | undefinedКоллбек для сброса признака ручного обновления
onErrorChange(value: boolean) => voidКоллбек для передачи в хост информации об ошибке
onFetch(status: FETCH_STATUSES) => voidКоллбек для передачи статуса загрузки данных
Что должен делать Widget
  • рендерить содержимое виджета;
  • использовать moduleData как основной payload своей конфигурации;
  • использовать variables, from, to для запросов данных или фильтрации;
  • при необходимости сигнализировать хосту через onErrorChange;
  • при необходимости обновлять status через onFetch.
Структура VariableValues

VariableValues это словарь:

  • ключ — имя переменной;
  • значение — string | string[] | number | null.

Это означает:

  • string — одиночное строковое значение;
  • string[] — множественный выбор;
  • number — числовое значение;
  • null — переменная пока не выбрана или очищена.
Значения FETCH_STATUSES
ЗначениеЧто означает
''состояние по умолчанию, без явного результата
'fetchLinerSuccess'успешная загрузка данных
'fetchLinerError'ошибка загрузки данных
к сведению

В исходном коде платформы moduleData хранится без фиксированной схемы. На публичном контракте его следует трактовать как JSON-совместимый объект, структура которого полностью определяется самим модулем.

WidgetEditor

Назначение:

  • отображает редактор виджета;
  • позволяет изменить произвольные настройки moduleData;
  • может показывать preview, форму, мастер настройки, конструктор, таблицы, drag-and-drop и любую другую логику.

Статус:

  • опционален, но нужен, если модуль должен поддерживать редактирование из UI.

Входные параметры компонента WidgetEditor

Компонент WidgetEditor получает объект props со следующими полями:

ПолеТипНазначение
dashboardPropsIDashboardWidgetPropsДиапазон времени, интервал обновления и размеры, унаследованные от дашборда
moduleIdstringИдентификатор подключенного модуля
moduleExternalModule | nullПолный объект модуля с metadata и settings
palettesIPalette[]Палитры, доступные в системе
widgetIModuleWidgetData | nullТекущее сохраненное состояние редактируемого виджета модуля, если оно есть
widgetIdstring | undefinedИдентификатор виджета при редактировании существующего
refreshClickedbooleanПризнак, что хост запросил обновление preview или данных
generalFieldsIField[]Общие поля стандартного редактора, если модуль хочет их переиспользовать
variablesVariableValues | nullТекущие значения переменных дашборда
onRefreshChange(value: boolean) => voidКоллбек для сброса или изменения состояния refresh
onChange() => voidКоллбек, которым модуль сообщает хосту, что данные редактора изменились
Что должен делать WidgetEditor
  • позволять пользователю менять moduleData будущего виджета;
  • использовать widget, если нужно редактировать уже сохраненный виджет;
  • использовать dashboardProps и variables, если нужен preview или зависимая конфигурация;
  • вызывать onChange, когда модуль считает, что данные редактора изменились и хост должен обновить dirty-state или preview.
Структура IDashboardWidgetProps
ПолеТипЧто означает
fromstring | undefinedНачало временного диапазона для preview и вычислений
tostring | undefinedКонец временного диапазона
refreshTimenumberИнтервал автообновления в миллисекундах
colsnumber | undefinedТекущая ширина виджета в колонках сетки
rowsnumber | undefinedТекущая высота виджета в строках сетки
Структура ExternalModule
ПолеТипЧто означает
idstringИдентификатор модуля в системе
namestringТехническое имя модуля
versionstringВерсия модуля
baseUrlstringБазовый URL backend-части или API модуля
buildTimestampstring | undefinedСлужебная метка сборки для cache busting
frontendHoststringБазовый host frontend-артефактов модуля
scopestringИмя глобального объекта window[scope]
modulestringВнутренний идентификатор типа модуля
isActivebooleanАктивен ли модуль в системе
canAddToDashboardboolean | undefinedРазрешено ли создавать виджеты этого модуля на дашборде
entryPointstringПуть к JS entry bundle модуля
componentsstring[]Список имен компонентов, доступных в registry
createdAtstringДата создания записи модуля
updatedAtstringДата последнего изменения записи модуля
settingsanyСохраненные настройки модуля
titleLocaleArrayItem[]Локализованное отображаемое имя
descriptionLocaleArrayItem[]Локализованное описание
Структура LocaleArrayItem
ПолеТипЧто означает
langstringКод языка, например ru-RU или en-US
valuestringЗначение строки для указанного языка
Структура IPalette
ПолеТипЧто означает
_idstring | undefinedИдентификатор палитры
namestringИмя палитры
colorsstring[]Список цветов палитры
createdAtstringДата создания палитры
updatedAtstringДата изменения палитры
createdByIShortUser | undefinedПользователь, создавший палитру
updatedByIShortUser | undefinedПользователь, изменивший палитру последним
isEditableboolean | undefinedМожно ли редактировать палитру в текущем контексте
Структура IShortUser
ПолеТипЧто означает
_idstringИдентификатор пользователя
usernamestringЛогин пользователя
emailstringEmail пользователя
rolestringРоль пользователя
Структура IModuleWidgetData
ПолеТипЧто означает
dashboardIdstringИдентификатор дашборда, к которому относится виджет
titlestringЗаголовок виджета
descriptionstring | undefinedОписание виджета
refreshTimenumber | string | null | undefinedСобственный интервал обновления виджета, если задан
fromstring | null | undefinedСобственное начало временного диапазона, если задано
tostring | null | undefinedСобственный конец временного диапазона, если задан
widgetType'module'Тип виджета для модульного сценария
createdAtstring | undefinedДата создания виджета
createdBystring | undefinedИдентификатор автора виджета
moduleIdstringИдентификатор подключенного модуля
moduleDataRecord<string, unknown>Сохраненные данные виджета модуля
Структура IField
ПолеТипЧто означает
titlestring | undefinedЗаголовок поля
namestringКлюч поля в форме
rulesobject | object[]Правила валидации
defaultValuestring | boolean | number | undefinedЗначение по умолчанию
propsIFieldProps | undefinedДополнительные параметры поля
controlWidthnumber | null | undefinedШирина контрола
noWrapperboolean | undefinedНужно ли отключить стандартную обертку
isHorizontalboolean | undefinedНужно ли горизонтальное расположение
requiredMarkboolean | undefinedНужно ли показывать признак обязательности
helpTextstring | undefinedПодсказка под полем
typestring | undefinedТип поля
childrenIField[] | undefinedВложенные поля
Структура IFieldProps
ПолеТипЧто означает
classNamestring | undefinedCSS-класс поля
placeholderstring | string[] | undefinedPlaceholder или локализуемые placeholder-значения
titleButtonstring | undefinedПодпись кнопки, если поле использует кнопку
itemsIOption[] | ISelectOption[] | undefinedНабор опций для выбора
titlestring | undefinedВнутренний заголовок контрола
disabledboolean | undefinedЗаблокировано ли поле
rowsnumber | undefinedКоличество строк для textarea
fieldsIField[] | undefinedВложенные поля для составных контролов
type'text' | 'object' | 'json' | 'array' | 'password' | 'email' | undefinedПодтип контрола
optionsISelect[] | undefinedОпции select или radio-контрола
minnumber | undefinedМинимальное значение
maxnumber | undefinedМаксимальное значение
stepnumber | undefinedШаг для числового значения
readOnlyboolean | undefinedРежим только для чтения
labelstring | undefinedТекстовая подпись
Что сейчас содержит generalFields

В текущем хосте в generalFields передаются два описания полей:

  • поле title — заголовок виджета;
  • поле description — описание виджета.

Settings

Назначение:

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

Статус:

  • опционален.
к сведению

В текущем интерфейсе настройки модуля часто отображаются как список соединений, но это не обязательный формат. Модуль может использовать произвольную форму настроек и любую удобную структуру settings.

Входные параметры компонента Settings

Компонент Settings получает объект props со следующими полями:

ПолеТипНазначение
settingsanyТекущее сохраненное состояние настроек модуля
moduleIdstringИдентификатор подключенного модуля в системе
onChange(data: any) => voidКоллбек для передачи нового значения настроек в хост
Что должен делать Settings
  • рендерить произвольную форму настроек;
  • вызывать onChange(newSettings) при изменении;
  • не выполнять прямое сохранение в систему самостоятельно, если это не предусмотрено вашей логикой;
  • считать settings единственным источником текущего состояния, загружаемого из хоста.

Контракт backend модуля

Backend модуля может быть реализован произвольно.

Графиня не требует фиксированного набора бизнес-endpoint-ов для модулей, в отличие от плагинов-источников данных.

Есть только две обязательные оговорки:

  • модуль должен отдавать GET /api/v1/well-known;
  • если frontend модуля общается с backend через backend Графини, то при proxy-вызовах в запрос может быть добавлен moduleContext.

Во многих сценариях модуль это именно full-stack расширение: frontend отвечает за UI и редакторы, а backend модуля отвечает за бизнес-логику, доступ к внешним системам, агрегацию данных и интерпретацию moduleContext.

Что такое moduleContext

Backend Графини умеет проксировать запросы модуля на module.baseUrl и для JSON API-запросов может автоматически подмешивать:

  • moduleContext

В moduleContext попадает содержимое сохраненных settings модуля.

Это позволяет модулю:

  • хранить свои настройки в системе Графини;
  • использовать их в собственном backend без необходимости повторно передавать их вручную в каждом запросе;
  • строить произвольный backend-контракт поверх собственного API.

Пример тела запроса, которое может получить backend модуля

{
"payload": {
"query": "summary"
},
"moduleContext": {
"connections": [
{
"name": "main",
"url": "https://api.example.com",
"token": "encrypted-token"
}
]
}
}
к сведению

Форма moduleContext никак не стандартизирована на уровне платформы. Это просто объект с настройками модуля, который был сохранен через Settings.

Proxy через backend Графини

Если frontend модуля не должен ходить к своему backend напрямую, можно использовать backend-proxy Графини.

Форма proxy-маршрута:

  • /api/v1/modules/{moduleId}/proxy/{path}

Особенности proxy:

  • запрос проксируется на module.baseUrl;
  • путь и query string сохраняются;
  • для JSON-запросов backend Графини может добавить в body поле moduleContext;
  • заголовок Accept-Language передается дальше в backend модуля.

Пример

Если:

  • moduleId = 123
  • module.baseUrl = https://module-api.example.com/api

и frontend модуля вызывает:

  • POST /api/v1/modules/123/proxy/report/summary

то backend Графини проксирует запрос на:

  • https://module-api.example.com/api/report/summary

Если тело запроса JSON, модуль может дополнительно получить moduleContext.

подсказка

Используйте proxy, если backend модуля требует секреты, внутренние адреса, централизованную аутентификацию или если вы не хотите решать CORS на стороне браузера.

Допустимая архитектура backend модуля

Вы можете выбрать любой вариант:

  • модуль полностью frontend-only и не имеет собственного backend;
  • модуль обращается напрямую к своему backend из браузера;
  • модуль обращается к своему backend через proxy backend-а Графини;
  • модуль использует смешанную схему, где часть запросов идет напрямую, а часть через proxy.

При выборе схемы учитывайте:

  • CORS;
  • безопасность токенов и секретов;
  • необходимость скрыть внутренние endpoint-ы;
  • необходимость использовать moduleContext.

Рекомендации по реализации

Локализация

Если модуль должен быть мультиязычным, рекомендуется:

  • хранить локализуемые метаданные уже в well-known;
  • использовать @grafinya/frontend-shared для работы с локализацией в UI;
  • учитывать возможный заголовок Accept-Language, если ваш backend строит локализованные ответы.

UI и UX

Рекомендуется:

  • использовать компоненты @takelage/ui для совместимого внешнего вида;
  • использовать @grafinya/frontend-shared для общих паттернов интерфейса;
  • не полагаться на внутренние нестабильные реализации хоста;
  • считать props компонентов единственным публичным контрактом.

Стили

Рекомендуется:

  • собирать CSS отдельно рядом с JS bundle;
  • избегать глобальных селекторов;
  • минимизировать риск конфликтов имен классов с хостом.

Минимальный чеклист

  • реализован GET /api/v1/well-known;
  • в well-known заполнены name, title, description, version, entryPoint, frontendHost, scope, components;
  • JS bundle регистрирует window[scope];
  • объект window[scope] содержит registry;
  • в registry есть как минимум Widget, если модуль должен отображаться на дашборде;
  • сборка совместима с React 18;
  • версии @takelage/ui и @grafinya/frontend-shared не ниже поддерживаемых в текущем хосте;
  • модуль корректно работает без предположений о конкретной внутренней реализации настроек;
  • если используется backend, он понимает moduleContext или игнорирует его безопасным образом.