Документация по разработке модулей
Эта документация описывает, как создать модуль для Графини по публичному контракту.
Модуль в текущей реализации нужен для работы на дашборде. Он может включать:
- frontend-часть с визуальными компонентами;
- собственный backend или API-слой;
- произвольную внутреннюю бизнес-логику и собственную модель данных.
Графиня ожидает от модуля минимальный внешний контракт для регистрации, загрузки frontend-части и, при необходимости, обмена контекстом с backend-частью модуля.
Готовый пример
В документацию добавлен учебный пример модуля:
- архив: module-empty-project.zip
В архиве есть:
- минимальный набор обязательных библиотек;
.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 обязателен для модуля, который должен отображаться на дашборде.
Общий порядок подключения
- Разработчик публикует модуль по своему URL.
- Графиня запрашивает
GET {baseUrl}/api/v1/well-known. - Из ответа well-known Графиня получает метаданные модуля:
name,title,description,version,entryPoint,frontendHost,scope,componentsи другие поля. - После сохранения модуля в системе frontend Графини загружает JS по адресу
new URL(entryPoint, frontendHost). - Если рядом с JS существует CSS с тем же именем файла, Графиня попытается подключить и его.
- После загрузки JS в
window[scope]должен появиться объект модуля. - Из этого объекта Графиня берет
registryи загружает нужный компонент по имени.
Обязательные frontend-зависимости
Для совместимости с текущим frontend-хостом используйте версии не ниже тех, которые сейчас применяются в Графине:
reactне ниже18.3.1react-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.React | react | 18.3.1 | Базовый React runtime для всех компонентов модуля |
window.ReactDOM | react-dom | 18.2.x | Рендеринг React-компонентов и совместимость с DOM API |
window.TakelageUI | @takelage/ui | 1.17.1 | UI-компоненты, формы, кнопки, layout и общий визуальный стиль |
window.FrontendShared | @grafinya/frontend-shared | 1.3.53 | Общие типы, инфраструктурные компоненты, локализация и утилиты платформы |
window.ReactHookForm | react-hook-form | 7.54.2 | Работа с формами и контроллерами полей |
window.DnDKitCore | @dnd-kit/core | 6.3.1 | Базовые drag-and-drop primitives |
window.DnDKitSortable | @dnd-kit/sortable | 10.0.0 | Сортировка элементов drag-and-drop |
window.DnDKitUtilities | @dnd-kit/utilities | 3.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, указанного при подключении модуля.
Минимально необходимые поля
nametitledescriptionversionentryPointfrontendHostscopecomponents
На текущий момент критически важны как минимум:
namecomponentsentryPoint
Но для корректного 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-параметры:
versionbuildTimestamp
Пример итогового 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 со следующими полями:
| Поле | Тип | Назначение |
|---|---|---|
moduleId | string | Идентификатор модуля |
moduleData | Record<string, unknown> | Данные виджета, сохраненные модулем |
palette | string[] | Текущая палитра цветов |
id | string | Идентификатор виджета |
variables | VariableValues | null | Значения переменных дашборда |
from | string | undefined | Левая граница временного диапазона |
to | string | undefined | Правая граница временного диапазона |
title | string | undefined | Заголовок виджета |
cols | number | undefined | Ширина виджета в колонках |
rows | number | undefined | Высота виджета в строках |
isDashboardEdit | boolean | Признак режима редактирования дашборда |
isWidgetEdit | boolean | Признак режима редактирования самого виджета |
isRefreshClicked | boolean | Признак ручного обновления |
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 со следующими полями:
| Поле | Тип | Назначение |
|---|---|---|
dashboardProps | IDashboardWidgetProps | Диапазон времени, интервал обновления и размеры, унаследованные от дашборда |
moduleId | string | Идентификатор подключенного модуля |
module | ExternalModule | null | Полный объект модуля с metadata и settings |
palettes | IPalette[] | Палитры, доступные в системе |
widget | IModuleWidgetData | null | Текущее сохраненное состояние редактируемого виджета модуля, если оно есть |
widgetId | string | undefined | Идентификатор виджета при редактировании существующего |
refreshClicked | boolean | Признак, что хост запросил обновление preview или данных |
generalFields | IField[] | Общие поля стандартного редактора, если модуль хочет их переиспользовать |
variables | VariableValues | null | Текущие значения переменных дашборда |
onRefreshChange | (value: boolean) => void | Коллбек для сброса или изменения состояния refresh |
onChange | () => void | Коллбек, которым модуль сообщает хосту, что данные редактора изменились |
Что должен делать WidgetEditor
- позволять пользователю менять
moduleDataбудущего виджета; - использовать
widget, если нужно редактировать уже сохраненный виджет; - использовать
dashboardPropsиvariables, если нужен preview или зависимая конфигурация; - вызывать
onChange, когда модуль считает, что данные редактора изменились и хост должен обновить dirty-state или preview.
Структура IDashboardWidgetProps
| Поле | Тип | Что означает |
|---|---|---|
from | string | undefined | Начало временного диапазона для preview и вычислений |
to | string | undefined | Конец временного диапазона |
refreshTime | number | Интервал автообновления в миллисекундах |
cols | number | undefined | Текущая ширина виджета в колонках сетки |
rows | number | undefined | Текущая высота виджета в строках сетки |
Структура ExternalModule
| Поле | Тип | Что означает |
|---|---|---|
id | string | Идентификатор модуля в системе |
name | string | Техническое имя модуля |
version | string | Версия модуля |
baseUrl | string | Базовый URL backend-части или API модуля |
buildTimestamp | string | undefined | Служебная метка сборки для cache busting |
frontendHost | string | Базовый host frontend-артефактов модуля |
scope | string | Имя глобального объекта window[scope] |
module | string | Внутренний идентификатор типа модуля |
isActive | boolean | Активен ли модуль в системе |
canAddToDashboard | boolean | undefined | Разрешено ли создавать виджеты этого модуля на дашборде |
entryPoint | string | Путь к JS entry bundle модуля |
components | string[] | Список имен компонентов, доступных в registry |
createdAt | string | Дата создания записи модуля |
updatedAt | string | Дата последнего изменения записи модуля |
settings | any | Сохраненные настройки модуля |
title | LocaleArrayItem[] | Локализованное отображаемое имя |
description | LocaleArrayItem[] | Локализованное описание |
Структура LocaleArrayItem
| Поле | Тип | Что означает |
|---|---|---|
lang | string | Код языка, например ru-RU или en-US |
value | string | Значение строки для указанного языка |
Структура IPalette
| Поле | Тип | Что означает |
|---|---|---|
_id | string | undefined | Идентификатор палитры |
name | string | Имя палитры |
colors | string[] | Список цветов палитры |
createdAt | string | Дата создания палитры |
updatedAt | string | Дата изменения палитры |
createdBy | IShortUser | undefined | Пользователь, создавший палитру |
updatedBy | IShortUser | undefined | Пользователь, изменивший палитру последним |
isEditable | boolean | undefined | Можно ли редактировать палитру в текущем контексте |
Структура IShortUser
| Поле | Тип | Что означает |
|---|---|---|
_id | string | Идентификатор пользователя |
username | string | Логин пользователя |
email | string | Email пользователя |
role | string | Роль пользователя |
Структура IModuleWidgetData
| Поле | Тип | Что означает |
|---|---|---|
dashboardId | string | Идентификатор дашборда, к которому относится виджет |
title | string | Заголовок виджета |
description | string | undefined | Описание виджета |
refreshTime | number | string | null | undefined | Собственный интервал обновления виджета, если задан |
from | string | null | undefined | Собственное начало временного диапазона, если задано |
to | string | null | undefined | Собственный конец временного диапазона, если задан |
widgetType | 'module' | Тип виджета для модульного сценария |
createdAt | string | undefined | Дата создания виджета |
createdBy | string | undefined | Идентификатор автора виджета |
moduleId | string | Идентификатор подключенного модуля |
moduleData | Record<string, unknown> | Сохраненные данные виджета модуля |
Структура IField
| Поле | Тип | Что означает |
|---|---|---|
title | string | undefined | Заголовок поля |
name | string | Ключ поля в форме |
rules | object | object[] | Правила валидации |
defaultValue | string | boolean | number | undefined | Значение по умолчанию |
props | IFieldProps | undefined | Дополнительные параметры поля |
controlWidth | number | null | undefined | Ширина контрола |
noWrapper | boolean | undefined | Нужно ли отключить стандартную обертку |
isHorizontal | boolean | undefined | Нужно ли горизонтальное расположение |
requiredMark | boolean | undefined | Нужно ли показывать признак обязательности |
helpText | string | undefined | Подсказка под полем |
type | string | undefined | Тип поля |
children | IField[] | undefined | Вложенные поля |
Структура IFieldProps
| Поле | Тип | Что означает |
|---|---|---|
className | string | undefined | CSS-класс поля |
placeholder | string | string[] | undefined | Placeholder или локализуемые placeholder-значения |
titleButton | string | undefined | Подпись кнопки, если поле использует кнопку |
items | IOption[] | ISelectOption[] | undefined | Набор опций для выбора |
title | string | undefined | Внутренний заголовок контрола |
disabled | boolean | undefined | Заблокировано ли поле |
rows | number | undefined | Количество строк для textarea |
fields | IField[] | undefined | Вложенные поля для составных контролов |
type | 'text' | 'object' | 'json' | 'array' | 'password' | 'email' | undefined | Подтип контрола |
options | ISelect[] | undefined | Опции select или radio-контрола |
min | number | undefined | Минимальное значение |
max | number | undefined | Максимальное значение |
step | number | undefined | Шаг для числового значения |
readOnly | boolean | undefined | Режим только для чтения |
label | string | undefined | Текстовая подпись |
Что сейчас содержит generalFields
В текущем хосте в generalFields передаются два описания полей:
- поле
title— заголовок виджета; - поле
description— описание виджета.
Settings
Назначение:
- отображает настройки самого подключенного модуля;
- позволяет редактировать
settings, которые затем сохраняются в конфигурации модуля; - может использоваться для любой структуры данных.
Статус:
- опционален.
В текущем интерфейсе настройки модуля часто отображаются как список соединений, но это не обязательный формат. Модуль может использовать произвольную форму настроек и любую удобную структуру settings.
Входные параметры компонента Settings
Компонент Settings получает объект props со следующими полями:
| Поле | Тип | Назначение |
|---|---|---|
settings | any | Текущее сохраненное состояние настроек модуля |
moduleId | string | Идентификатор подключенного модуля в системе |
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 = 123module.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или игнорирует его безопасным образом.