Порталы — API Cookbook: создание портала и добавление виджетов¶
Полная инструкция по программному управлению порталами через admin API. Все примеры — рабочие, проверены на продакшне.
Discovery: все портальные API — MCP-серверы
/mcp-admin-api-portals,/mcp-admin-api-portal-blocks,/mcp-admin-api-portals-includes. Полный каталог:docs/reference/api/mcp-catalog.md.
См. также: Туториал «Данные из внешнего API на портале» — сквозной пример Lua → публикация → SmartHtml-виджет (курсы валют ЦБ). Архивная страница в admin-manual: tutorial-lua-api-portal.md.
Аутентификация: все admin-эндпоинты требуют [RoleAdmin] (God-user). Авторизация через PAT:
-H "1F-Pat: {token}"
1. Создание портала¶
Эндпоинт: POST /app/v1.0/api/portal/addAdminTemplate
Контроллер: PortalController.AddAdminTemplate
DTO: AddAdminTemplateRequestBodyDto
curl -s -X POST "$HOST/app/v1.0/api/portal/addAdminTemplate" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"Name": "Dev Burn-up Dashboard",
"isCustomerZone": false,
"ShowHeader": true,
"ModuleId": null
}'
Ответ: объект PortalGridTemplateDto с присвоенным id:
{
"id": 5071,
"name": "Dev Burn-up Dashboard",
"gridType": "Flex",
"isFixedMode": true,
"showHeader": true
}
Портал создаётся с GridType=Flex. Для CssGrid (современный layout) — обновить через updateAdminTemplate (шаг 4).
| Поле | Тип | Описание |
|---|---|---|
Name |
string | Название портала |
isCustomerZone |
bool? | Клиентская зона (обычно false) |
ShowHeader |
bool | Показывать заголовок портала |
ModuleId |
int? | Привязка к модулю (null = без привязки) |
2. Создание блока (виджета)¶
Эндпоинт: POST /api/portals/block/add
Контроллер: BlockController.Add (v2.0)
DTO: PortalBlockCreateRequestDto
curl -s -X POST "$HOST/api/portals/block/add" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"Name": "Burn-up спринта",
"TypeId": 33,
"IsRequired": false,
"IsEnabled": true,
"ShowFooter": false,
"TemplateId": 5071,
"ContextTypeId": 0,
"IsFilterHidden": true,
"IsHideableByUser": false,
"IsHeaderHidden": false,
"CanExpand": false,
"WidgetNameType": 0,
"WidgetColorMode": 0,
"GroupsIds": [1620],
"SectionsIds": []
}'
Ответ: 13461 (int — ID созданного блока).
Ключевые поля¶
| Поле | Тип | Описание |
|---|---|---|
Name |
string | Название виджета |
TypeId |
int | Тип блока (см. таблицу ниже) |
TemplateId |
int | ID портала (не portalId!) — поле названо templateId в DTO |
GroupsIds |
int[] | Группы доступа — обязательно, иначе виджет не виден |
SectionsIds |
int[] | Секции портала (для группировки) |
ContextTypeId |
byte | 0 = Портал |
IsEnabled |
bool | Включён ли виджет |
IsHideableByUser |
bool | Может ли пользователь скрыть |
IsHeaderHidden |
bool | Скрыть заголовок виджета |
CanExpand |
bool | Разворачивается на весь экран |
WidgetNameType |
enum | 0=Fixed, 1=Smart |
WidgetColorMode |
enum | 0=Default, 1=Transparent |
Gotchas блоков¶
| Проблема | Причина | Решение |
|---|---|---|
| Над виджетом показывается старое/неправильное название | Name блока отображается пользователям как заголовок виджета. Это НЕ внутреннее имя — оно видно в UI |
Обновить Name через POST /api/portals/block/{blockId}/update с {"Name": "Новое имя"} |
| Хочу убрать заголовок виджета совсем | По умолчанию IsHeaderHidden: false |
Установить IsHeaderHidden: true при создании или обновлении блока |
| Два заголовка: портальный ShowHeader + виджетный Name | ShowHeader портала и Name блока — независимые. Оба могут отображаться одновременно |
Выбрать один: либо ShowHeader: false на портале, либо IsHeaderHidden: true на блоке |
| Дублирование: Name блока + заголовок в SmartHtml template | Если SmartHtml содержит свой <h1> и блок показывает Name — будет два заголовка |
Убрать заголовок из SmartHtml template — использовать Name блока как единственный заголовок |
Типы блоков (GridBlockType enum)¶
| TypeId | Тип | Описание |
|---|---|---|
| 0 | Buttons | Блок кнопок |
| 1 | HTML | Блок HTML |
| 2 | Subcat | Выборка задач (категория) |
| 5 | SmartBlock | Смарт-блок |
| 18 | Chart | График |
| 19 | TasksSearch | Поиск задач |
| 20 | Feed | Лента событий |
| 21 | Menu | Навигационное меню |
| 22 | Table | Таблица |
| 29 | Pivot | Сводная таблица |
| 30 | Gantt | Диаграмма Ганта |
| 33 | SmartHtml | Smart HTML (самый гибкий) |
| 34 | CalendarSmartHtml | Календарь Smart HTML |
| 38 | Calendar | Календарь |
| 40 | Counters | Счётчики |
| 44 | Filter | Виджет фильтров |
| 45 | Navigation | Навигация |
Полный enum: Valhalla.Integration/Enums/Portal/GridBlockType.cs.
Gotchas создания блоков¶
| Проблема | Причина | Решение |
|---|---|---|
| Блок создан, но не виден | Нет GroupsIds |
Передать GroupsIds: [1620] при создании |
| TemplateId при создании ИГНОРИРУЕТСЯ | CreatePortalBlock перезаписывает TemplateId = anyPortalId (строка 172 PortalGridBlockService.cs) — блок попадает на случайный портал |
После создания вызвать POST /api/portals/block/{blockId}/update с правильным TemplateId. Ожидаем фикс: — после него этот workaround не нужен |
| Блок на портале, но не рендерится | Блок не добавлен в mesh | См. шаг 4 — добавление в mesh. TemplateId — для группировки в админке, mesh определяет рендеринг |
3. Настройка Smart HTML шаблона¶
После создания блока SmartHtml — настроить HTML-шаблон и (опционально) смарт-выражения.
Эндпоинт: POST /api/admin/portals/blocks/advsets/smart-html?blockId={blockId}
Контроллер: BlockAdvSetsSmartHtmlController.SaveData
DTO: SmartHtmlData {Template, Smarts}
curl -s -X POST "$HOST/api/admin/portals/blocks/advsets/smart-html?blockId=13461" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"template": "<div id=\"burnup-chart\" style=\"width:100%;min-height:400px;\"></div>",
"smarts": ""
}'
Ответ: HTTP 204 No Content.
| Поле | Тип | Описание |
|---|---|---|
template |
string | HTML-шаблон (mustache-совместимый) |
smarts |
string | JSON-конфиг смарт-выражений (пустая строка если не нужны) |
Важно:
blockId— query parameter, не в body.
4. Добавление блока в mesh портала (CssGrid)¶
Ожидаем упрощение: эндпоинт
add-to-mesh/remove-from-mesh(P2 вdocs/domains/portal/portal-api-improvements-spec.md) — один вызов вместо load→parse→modify→save. После реализации шаги 4a–4c заменяются одним POST.
SPA загружает блоки из mesh JSON — поля PortalGridTemplates.Mesh. Просто привязка блока к порталу через TemplateId недостаточна — блок должен быть явно перечислен в mesh.
4a. Загрузить текущий портал¶
curl -s -X POST "$HOST/app/v1.0/api/portal/adminTemplates" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"targetPortalId": 5071,
"WithTemplateContent": true,
"WithTemplateBlockIncludes": true
}'
Из ответа извлечь: result[0].template.{id, name, mesh, dashboard, gridType, isFixedMode, showHeader, moduleId}.
4b. Сформировать mesh с блоком¶
Формат mesh — JSON с массивом blocks[] и режимом размещения:
{
"blocks": [
{
"guid": "уникальный-uuid",
"blockIds": [13461],
"type": "widget",
"containerRules": {},
"sectionRules": {"type": "1/1"},
"sizeRules": {
"13461": {
"13461": {"type": "custom", "col": 2, "row": 2},
"type": "custom",
"col": 12,
"row": 4
}
}
}
],
"portalPlacingMode": "row"
}
Структура sizeRules — двухуровневая:
"sizeRules": {
"13461": { ← ключ = blockId (строка)
"13461": { ← вложенный ключ = blockId (мин. размеры / fallback)
"type": "custom",
"col": 2, "row": 2
},
"type": "custom", ← основной тип размера
"col": 12, ← ширина в колонках (из 12)
"row": 4 ← высота в рядах
}
}
col: 12, row: 4 = полная ширина, 4 ряда. Внешний уровень — размер блока на портале, внутренний — минимальные/свёрнутые размеры.
Дополнительные свойства блока в mesh:
| Поле | Тип | Описание |
|---|---|---|
guid |
string | Уникальный UUID записи в mesh |
blockIds |
int[] | ID блоков (обычно один) |
type |
string | "widget" или "section" |
startNewRow |
bool | Начать с новой строки |
sizeRules |
object | Размеры: col (1-12), row |
4c. Сохранить портал¶
Эндпоинт: POST /app/v1.0/api/portal/updateAdminTemplate
Контроллер: PortalController.UpdateAdminTemplate
DTO: UpdateTemplateRequestBodyDto
curl -s -X POST "$HOST/app/v1.0/api/portal/updateAdminTemplate" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"id": 5071,
"name": "Dev Burn-up Dashboard",
"isFixedMode": false,
"mesh": "{\"blocks\":[...],\"portalPlacingMode\":\"row\"}",
"dashboard": null,
"GridType": 2,
"ShowHeader": true,
"ModuleId": null
}'
Ответ: true (HTTP 200).
Обязательные поля updateAdminTemplate¶
Все поля обязательны — эндпоинт перезаписывает ВСЕ поля сущности:
| Поле | Тип | Описание |
|---|---|---|
id |
int | ID портала |
name |
string | Название (перезапишет!) |
isFixedMode |
bool? | Фиксированный режим |
mesh |
string | JSON mesh layout (строка!) |
dashboard |
string | JSON dashboard layout (null для CssGrid) |
GridType |
int | 0=Flex, 1=Dashboard, 2=CssGrid |
ShowHeader |
bool | Показывать заголовок |
ModuleId |
int? | Модуль |
GridType enum¶
| Значение | Тип | Описание |
|---|---|---|
| 0 | Flex | Старый тип, через templateJson |
| 1 | Dashboard | Dashboard layout |
| 2 | CssGrid | Современный, через mesh JSON |
Gotchas updateAdminTemplate¶
| Проблема | Причина | Решение |
|---|---|---|
| HTTP 500 | Не все поля переданы (name=null) | Передать ВСЕ поля, включая name, GridType и т.д. |
| Портал стал пустым | mesh не содержит blocks | Проверить JSON mesh, убедиться что blockIds заполнены |
| Название портала изменилось | name перезаписывается | Передавать текущее имя |
5. JS/CSS Include — создание и привязка¶
5a. Создание Include¶
Эндпоинт: POST /api/includes/add
curl -s -X POST "$HOST/api/includes/add" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"name": "Burn-up Sprint Chart",
"type": 1,
"content": "(function() { /* JS code */ })()"
}'
Ответ: {data: 5840} (ID include).
| Поле | Тип | Описание |
|---|---|---|
name |
string | Описание (в DTO — description) |
type |
int | 1=Js, 2=Css, 3=JsUrl |
content |
string | Содержимое (JS/CSS код или URL для JsUrl) |
Gotchas Includes¶
| Проблема | Причина | Решение |
|---|---|---|
| Перезаписан чужой Include | POST /api/includes/add возвращает ID в ответе, но если ID не извлечён — разработчик ищет «последний ID» через SQL и попадает на чужой Include. POST /api/includes/{id}/update перезаписывает content без проверки владельца |
Всегда извлекать ID из ответа POST /api/includes/add ({data: 5840}). Никогда не угадывать ID по SQL. Перед update — проверить description через SELECT description FROM Includes WHERE IncludeId = N |
| Include обновлён, но код не изменился | Баг: POST /api/includes/{id}/update иногда не обновляет Content |
Создать новый include через POST /api/includes/add, перепривязать к блоку, удалить старый |
| Тип сбросился на CSS (code не выполняется) | При update не передан Type: 1 — сбрасывается на 0 (CSS) |
Всегда передавать Type: 1 (JS) при обновлении |
| Include привязан к нескольким блокам | Один Include может быть на N блоках. Обновление затрагивает ВСЕ | Перед update проверить: SELECT PortalId FROM PortalIncludes WHERE IncludeId = N — убедиться что Include используется только вашим блоком |
5b. Привязка Include к блоку¶
Эндпоинт: POST /api/portals/block/{blockId}/includes/add?includeId={includeId}
Контроллер: BlockController.AddInclude (v2.0)
curl -s -X POST "$HOST/api/portals/block/13461/includes/add?includeId=5840" \
-H "1F-Pat: $PAT"
Ответ: HTTP 200.
Важно:
includeId— query parameter, не body. Запрос требует пустой body{}(без body — HTTP 400).
5c. Обновление Include¶
Эндпоинт: POST /api/includes/{includeId}/update
curl -s -X POST "$HOST/api/includes/5840/update" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"Description": "Burn-up Sprint Chart",
"Type": 1,
"Content": "(function() { /* updated JS */ })()"
}'
КРИТИЧНО: всегда передавать
Typeпри обновлении. Эндпоинт перезаписывает все поля. Если не передатьType: 1(JS), поле сбрасывается на0(CSS) — include перестаёт выполняться как JavaScript. SmartHtml рендерит CSS-includes через<style>, а JS — через<script>. При сбросе типа код попадёт в<style>и будет проигнорирован браузером.Минимальный безопасный вызов:
curl -s -X POST "$HOST/api/includes/5840/update" \ -H "1F-Pat: $PAT" -H "Content-Type: application/json" \ -d '{"Content": "..", "Type": 1}'Баг (март 2026):
Contentигнорируется при update.POST /api/includes/{id}/updateвозвращает 200, ноContentне перезаписывается —GET /api/includes/include/{id}отдаёт старое содержимое. Обновляется толькоDescription. Workaround: создать новый include черезPOST /api/includes/add, привязать к блоку, отвязать старый (DELETE /api/portals/block/{blockId}/includes/delete?includeId={includeId}— includeId в query, НЕ в path).
5d. Дизайн-система — правила для JS Include¶
реализовано — используйте
spaApi.loadApexChartsвместо CDN-загрузки. CDN-загрузка (loadApexCharts/ensureApexCharts) устарела. Существующие виджеты с guardif (window.ApexCharts) return cbпродолжат работать.
Канонический паттерн для ApexCharts в JS Include:
spaApi.loadApexCharts().then(function(ApexCharts) {
var chart = new ApexCharts(el, opts);
chart.render();
});
JS-код виджетов обязан соответствовать дизайн-системе 1Формы (docs/platform/frontend/design-system/portal-markup-guide.md).
Запрещено:
- Хардкод цветов (#005eff, rgba(0,0,0,0.6)) — только CSS-токены
- Инлайн font-size, font-weight — только классы vh-*
- Неполный fontFamily — только полный системный стек
Как читать токены в JS (для ApexCharts, Chart.js и т.д.):
function token(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
// Графики: серии данных
var primary = token('--surfacedata-primary'); // синий
var secondary = token('--surfacedata-secondary'); // мятный
// Текст на осях, легенда
var axisText = token('--onsurface-secondary');
// Сетка графика
var gridLine = token('--outline-shallow');
// Бейджи: container + oncontainer пара
var bgInfo = token('--container-info');
var textInfo = token('--oncontainer-info');
Типографика в HTML, генерируемом из JS:
// ПРАВИЛЬНО — класс vh-label-md задаёт 12px/16px, weight 500
'<span class="vh-label-md" style="background:' + token('--container-success') + '">'
// НЕПРАВИЛЬНО — инлайн font-size/weight
'<span style="font-size:12px; font-weight:500; background:#e0ffea">'
Полный системный шрифт (для chart-библиотек):
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Ubuntu, Oxygen, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif
Ключевые токены для виджетов:
| Назначение | Токен |
|---|---|
| Серии графика | --surfacedata-primary, --surfacedata-secondary, --surfacedata-dataset-* |
| Основной текст | --onsurface-primary |
| Вторичный текст (оси, мета) | --onsurface-secondary |
| Сетка графика | --outline-shallow |
| Бейдж info (фон/текст) | --container-info / --oncontainer-info |
| Бейдж success | --container-success / --oncontainer-success |
| Бейдж default | --container-default / --oncontainer-default |
| Ошибка | --onsurfaceextra-danger |
5e. Gotchas JS Include¶
| Проблема | Причина | Решение |
|---|---|---|
| Include не выполняется в SmartHtml | Include привязан к порталу, а не к блоку | Привязать к блоку: POST /api/portals/block/{blockId}/includes/add?includeId=N. SmartHtml выполняет только block-level includes (jsIncludesIds) |
| При SPA-навигации между вкладками — пустота | JS Include (IIFE) выполняется один раз при первом рендере блока. SPA client-side navigation не перезапускает скрипт | Использовать MutationObserver (паттерн ниже) |
| Двойная инициализация виджетов | MutationObserver срабатывает на уже инициализированные элементы | Маркировать атрибутом (data-hm-init), искать :not([data-hm-init]) |
| ApexCharts heatmap дёргается при клике | dataPointSelection переключает selected state и перерисовывает серии |
Использовать chart.events.click + animations: {enabled: false} + states: {active: {filter: {type: 'none'}}} |
| Include обновлён, но не работает | POST /api/includes/{id}/update без Type: 1 сбрасывает тип на CSS (0). Include инжектится как <style> вместо <script> |
Всегда передавать "Type": 1 при обновлении JS-includes (§5c) |
| CDN-библиотеки (d3, dagre) не грузятся | Корпоративный firewall блокирует CDN. Или loadScript вызывается до DOM ready |
Fallback: проксировать через свой сервер. Проверять typeof d3 !== 'undefined' перед использованием |
| Portal-level include не выполняется в SmartHtml | Portal-level includes загружаются через PortalIncludes, но SmartHtml-блок использует только PortalBlockIncludes (block-level) |
Привязать include к блоку, не к порталу. Portal-level includes работают для других типов виджетов |
Паттерн: SPA-safe инициализация виджетов через MutationObserver
JS Include в SmartHtml — IIFE, который выполняется один раз. Если пользователь переключается между порталами через вкладки (SPA navigation), новые DOM-элементы появляются без повторного вызова скрипта. Решение:
(function() {
function discoverAndInit() {
// Ищем только НЕинициализированные контейнеры
var containers = document.querySelectorAll('[data-source]:not([data-hm-init])');
if (!containers.length) return;
containers.forEach(function(c) {
c.setAttribute('data-hm-init', '1'); // маркер против двойной инит
initWidget(c);
});
}
// Первый проход — текущий DOM
discoverAndInit();
// SPA navigation — ловим появление новых элементов
new MutationObserver(function(mutations) {
for (var i = 0; i < mutations.length; i++) {
var added = mutations[i].addedNodes;
for (var j = 0; j < added.length; j++) {
var node = added[j];
if (node.nodeType !== 1) continue;
if ((node.hasAttribute && node.hasAttribute('data-source')
&& !node.hasAttribute('data-hm-init'))
|| (node.querySelector
&& node.querySelector('[data-source]:not([data-hm-init])'))) {
discoverAndInit();
return; // один вызов на batch мутаций
}
}
}
}).observe(document.body, {childList: true, subtree: true});
function initWidget(container) { /* ... */ }
})();
Ключевые моменты:
- data-hm-init — атрибут-маркер, предотвращает двойную инициализацию
- MutationObserver с {childList: true, subtree: true} — ловит вставку элементов на любом уровне
- return после discoverAndInit — один вызов на batch мутаций (оптимизация)
- Паттерн универсален: заменить [data-source] на свой селектор контейнера
5f. Список includes блока¶
# Получить привязанные
GET /api/portals/block/{blockId}/includes
# Доступные для добавления
GET /api/portals/block/{blockId}/includes/available-for-add
# Удалить привязку
DELETE /api/portals/block/{blockId}/includes/delete?includeId={includeId}
Portal-level includes (альтернатива block-level)¶
Includes можно привязать к порталу целиком (работают на всех блоках портала):
# Привязать к порталу
POST /api/admin/portals/{portalId}/includes/add?includeId={includeId}
# Список includes портала
GET /api/admin/portals/{portalId}/includes
# Доступные для добавления
GET /api/admin/portals/{portalId}/includes/available-for-add
# Удалить
DELETE /api/admin/portals/{portalId}/includes/delete?includeId={includeId}
Разница: portal-level includes загружаются через PortalIncludes, block-level — через PortalBlockIncludes.
Для SmartHtml (TypeId=33) — только block-level. SmartHtml-блок при рендеринге читает
jsIncludesIdsизPortalBlockIncludes. Portal-level includes (PortalIncludes) не попадают в SmartHtml-блоки. Если include привязан к порталу, но не к блоку — SmartHtml его не увидит.
6. Обновление блока¶
Эндпоинт: POST /api/portals/block/{blockId}/update
DTO: PortalBlockUpdateRequestDto
curl -s -X POST "$HOST/api/portals/block/13461/update" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"Name": "Burn-up спринта",
"TypeId": 33,
"IsEnabled": true,
"IsRequired": false,
"ShowFooter": false,
"TemplateId": 5071,
"ContextTypeId": 0,
"IsFilterHidden": true,
"IsHideableByUser": false,
"IsHeaderHidden": false,
"CanExpand": false,
"WidgetNameType": 0,
"WidgetColorMode": 0,
"GroupsIds": [1620],
"SectionsIds": []
}'
7. Переименование портала¶
Эндпоинт: POST /app/v1.0/api/portal/rename
curl -s -X POST "$HOST/app/v1.0/api/portal/rename" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"PortalId": 5071,
"NewName": "Dev Burn-up Dashboard",
"ShowHeader": true
}'
8. Удаление портала¶
Эндпоинт: DELETE /api/admin/portals/{portalId}
curl -s -X DELETE "$HOST/api/admin/portals/5071" \
-H "1F-Pat: $PAT"
9. Удаление блока¶
Эндпоинт: DELETE /api/portals/block/{blockId}/delete
curl -s -X DELETE "$HOST/api/portals/block/13461/delete" \
-H "1F-Pat: $PAT"
Удаление блока не проверяет mesh — блок удалится, но останется «призраком» в mesh JSON. SPA проигнорирует несуществующий blockId, но mesh лучше почистить вручную.
10. Доступ к виджету — группы¶
Доступ настраивается на уровне виджета (блока), не портала. Без GroupsIds виджет не виден обычным пользователям.
При создании: передать GroupsIds: [1620] в PortalBlockCreateRequestDto.
При обновлении: передать GroupsIds: [1620] в PortalBlockUpdateRequestDto через POST /api/portals/block/{blockId}/update.
Проверка: пользователь видит виджет, если состоит в одной из групп блока (напрямую или через ассистирование).
11. Доступ к публикации — ExternalObject¶
При создании публикации автоматически создаётся ExternalObject (GUID возвращается в ответе). Без настройки доступа — 403 Forbidden при вызове.
Контроллер: ExternalObjectsController (app/v1.2/api)
DTO: ExternalObjectFullDto (наследует ExternalObjectDto)
Получить текущие настройки¶
GET /app/v1.2/api/externalObjects/{guid}/full
Ответ:
{
"id": 1164,
"guid": "acc8ef91-...",
"description": "Публикации: gitlab-dashboard",
"visibleToAnyone": false,
"isAnonymous": false,
"externalObjectsViewRights": [],
"externalObjectsSpecialRights": [],
"externalObjectsSmartExpressionRight": null
}
Открыть для всех авторизованных¶
POST /app/v1.2/api/externalObjects/{guid}
{"visibleToAnyone": true}
Ограничить группами¶
curl -s -X POST "$HOST/app/v1.2/api/externalObjects/{guid}" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"visibleToAnyone": false,
"isAnonymous": false,
"description": "Публикации: gitlab-dashboard",
"externalObjectsViewRights": [
{"externalObjectId": "{guid}", "groupId": 1620}
]
}'
Ответ: HTTP 204 No Content.
GUID — из ответа на создание публикации (POST /app/v1.2/api/publishedObjects → data.externalObjectId). Если GUID утерян — найти через GET /app/v1.2/api/publishedObjects/gridData (поле externalObjectId).
Gotchas ExternalObject¶
| Проблема | Причина | Решение |
|---|---|---|
| 403 при вызове публикации | ExternalObject access не настроен | visibleToAnyone: true или добавить группы |
| 500 при обновлении | Неправильный формат body. groupIds не существует |
Использовать externalObjectsViewRights: [{externalObjectId, groupId}] |
| GET возвращает 404 | Путь без /full |
Использовать GET ../externalObjects/{guid}/full |
| Пользователь видит виджет, но данных нет | Блок доступен (группы блока), но публикация закрыта | Настроить ExternalObject с теми же группами |
visibleToAnyone = true по умолчанию (DDL) |
Новые публикации видны всем | Для привилегированных операций — явно снять флаг, назначить группу |
Полная последовательность: портал + SmartHtml виджет с JS¶
Ожидаем упрощения (
docs/domains/portal/portal-api-improvements-spec.md): - шаг 3 исчезает (TemplateId будет работать при создании) - P2 (add-to-mesh) → шаг 7 сокращается до одного вызова - P4 (quick publication) → цепочка Script→Pack→Action→Publication сокращается до одного вызова
1. Создать портал
POST /app/v1.0/api/portal/addAdminTemplate
→ portalId
2. Создать блок SmartHtml
POST /api/portals/block/add
{TemplateId: portalId, TypeId: 33, GroupsIds: [1620], ...}
→ blockId
⚠️ TemplateId игнорируется при создании (баг, ждём фикс)
3. Привязать блок к правильному порталу (уйдёт после исправления)
POST /api/portals/block/{blockId}/update
{TemplateId: portalId, ...все остальные поля...}
4. Настроить HTML шаблон
POST /api/admin/portals/blocks/advsets/smart-html?blockId={blockId}
{template: "<div>...</div>", smarts: ""}
5. Создать JS Include
POST /api/includes/add
{name: "...", type: 1, content: "..."}
→ includeId
6. Привязать Include к блоку
POST /api/portals/block/{blockId}/includes/add?includeId={includeId}
7. Добавить блок в mesh портала (упростится после P2: add-to-mesh)
a. GET текущий: POST /app/v1.0/api/portal/adminTemplates
b. Добавить blockId в mesh.blocks[]
c. Сохранить: POST /app/v1.0/api/portal/updateAdminTemplate
{id: portalId, GridType: 2, mesh: JSON.stringify(mesh), ...}
Результат: https://{host}/spa/portal/{portalId}
Повторное использование виджета на другом портале¶
Один блок можно использовать на нескольких порталах — достаточно добавить его blockId в mesh нужного портала. SmartHtml-настройки, includes, группы доступа — всё сохраняется на уровне блока и работает везде, где он подключён.
1. Загрузить mesh целевого портала
POST /app/v1.0/api/portal/adminTemplates
2. Добавить существующий blockId в mesh.blocks[]
3. Сохранить
POST /app/v1.0/api/portal/updateAdminTemplate
Справочник эндпоинтов¶
| Операция | Метод | Эндпоинт |
|---|---|---|
| Создать портал | POST | /app/v1.0/api/portal/addAdminTemplate |
| Обновить портал (mesh, GridType) | POST | /app/v1.0/api/portal/updateAdminTemplate |
| Переименовать портал | POST | /app/v1.0/api/portal/rename |
| Удалить портал | DELETE | /api/admin/portals/{portalId} |
| Список порталов | GET | /api/admin/portals |
| Загрузить портал с блоками | POST | /app/v1.0/api/portal/adminTemplates |
| Создать блок | POST | /api/portals/block/add |
| Получить блок | GET | /api/portals/block/{blockId} |
| Обновить блок | POST | /api/portals/block/{blockId}/update |
| Удалить блок | DELETE | /api/portals/block/{blockId}/delete |
| SmartHtml: получить | GET | /api/admin/portals/blocks/advsets/smart-html?blockId=N |
| SmartHtml: сохранить | POST | /api/admin/portals/blocks/advsets/smart-html?blockId=N |
| Include: создать | POST | /api/includes/add |
| Include: обновить | POST | /api/includes/{id}/update |
| Include → блок: привязать | POST | /api/portals/block/{blockId}/includes/add?includeId=N |
| Include → блок: список | GET | /api/portals/block/{blockId}/includes |
| Include → блок: удалить | DELETE | /api/portals/block/{blockId}/includes/delete?includeId=N |
| Include → портал: привязать | POST | /api/admin/portals/{portalId}/includes/add?includeId=N |
| Include → портал: список | GET | /api/admin/portals/{portalId}/includes |
| Include → портал: удалить | DELETE | /api/admin/portals/{portalId}/includes/delete?includeId=N |
| Секции блоков | GET | /api/portals/block/sections |
| Типы блоков | GET | /api/portals/block/types |
10. Навигационный виджет (Type=45)¶
Navigation widget — таб-бар для переключения между связанными порталами.
Чтение текущих настроек¶
curl -s "$HOST/api/portals/block/{blockId}/type-params" \
-H "1F-Pat: $PAT"
Ответ содержит data.typeParams[] с единственным параметром NavigationWidgetsBlockSettings, поле value — JSON-строка с конфигурацией табов.
Настройка навигации¶
Эндпоинт: POST /api/portals/block/{blockId}/type-params/update
curl -s -X POST "$HOST/api/portals/block/{blockId}/type-params/update" \
-H "1F-Pat: $PAT" -H "Content-Type: application/json" \
-d '{
"typeParams": [{
"typedType": "String",
"type": "String",
"name": "NavigationWidgetsBlockSettings",
"title": "Navigation Widget Block Settings",
"description": null,
"required": true,
"editable": true,
"visible": false,
"userParam": false,
"userCanChange": false,
"availableOptions": null,
"value": "{JSON навигации — см. ниже}"
}]
}'
Формат value (JSON навигации)¶
{
"links": [
{"title": "Таб 1", "portalId": "5371", "order": "1", "children": []},
{"title": "Таб 2", "portalId": "5381", "order": "2", "children": []}
],
"navType": "tabs",
"stickyHeader": true
}
| Поле | Тип | Описание |
|---|---|---|
links[].title |
string | Текст таба |
links[].portalId |
string | ID целевого портала (строка!) |
links[].order |
string | Порядок (строка) |
links[].children |
array | Вложенные элементы (для dropdown) |
navType |
string | "tabs" — горизонтальные табы |
stickyHeader |
bool | Прилипание к верху при скролле |
Gotchas¶
| Проблема | Причина | Решение |
|---|---|---|
portalId должен быть строкой |
DTO ожидает string, не int | "portalId": "5371", не "portalId": 5371 |
POST /api/portals/block/{id}/update с TypeParams → 500 |
Этот endpoint не обрабатывает TypeParams | Использовать POST /api/portals/block/{id}/type-params/update |
| value — строка, не объект | value = JSON-encoded string внутри JSON | Сериализовать навигацию в строку: json.dumps(nav_config) |
Связанные документы¶
docs/domains/portal/admin.md— обзор администрирования порталовdocs/domains/portal/backend.md— backend-архитектура, контроллеры, сервисыdocs/domains/portal/data-flow.md— E2E диагностика загрузки порталаdocs/platform/frontend/design-system/portal-markup-guide.md— дизайн-система: токены, типографика, цвета, паттерны вёрстки виджетовdocs/projects/dev-process-portal/burnup-widget-spec.md— реализация burn-up виджета (пример)