Во время быстрого развития ИТ среды, многие её активные участники пользуются готовыми решениями обеспечивающими их определённым функционалом, который хотелось бы расширить. Но расширение продукта зачастую является либо платным, либо чрезвычайно затратным действием, требующим постоянного контроля доступной кодовой базы, адаптацией и корректировкой своей части для совместимости. Приходится создавать продукты размещаемые "рядом" и различными способами производить интеграцию. Попробуем изменить это с помощью минимальных затрат и расширить функционал Gitlab получив практически "бесшовную" интеграцию со своими продуктами.
Как устроен Gitlab ?
Для понимания того, что и как устроено, нужно определиться с топологией Gitlab как продукта в целом. Из основных частей стоит выделить:
puma - внутренний вебсервер, отвечает за вебинтерфейс и отрисовку контента
gitlab-workhorse - внутренний reverse-proxy, отвечающий за обработку внешних запросов пользователя, работает в связке с nginx и puma
gitlab-kas - модуль аутентификации
gitaly - отвечает за работу с репозиториями и предоставляет внутренний RPC интерфейс для различных типов взаимодействия с репозиториями
registry - отвечает за внутренний registry в котором хранятся контейнеры или пакеты
sidekiq отвечает за координацию внутренних процессов и увязку их в выполняемые по шедулеру задачи
postgres отвечает за хранение оперативной информации
сверху всего находится nginx, объединяющий все части для внешнего взаимодействия с пользователем
мониторинговые сервисы мы рассматривать не будем, т.к. они не входят в сферу наших интересов
В рамках статьи посвящённой расширению функционала Gitlab, нас будут интересовать модули puma и nginx.
Остановимся немного на puma. Puma это мощный веб сервер, написанный на ruby, использующий широкие возможности шаблонизации и обеспечивающий подготовку видимого пользователю контента, одновременно с жёсткой привязкой дальнейшей визуализации/интерактивности средствами vue.js и css. Шаблонизация страниц в Gitlab является практически универсальной и строится на haml шаблонах, поэтому структура почти всех страниц является единой и включает в себя:
базовые заголовки стилей в зависимости от выбранной пользователем темы
блок настроек для vue.js помещаемый в начало страницы и содержащий справочник настроек используемых для интерактива с пользователем
блок настроек для graphql запросов (часть страниц)
блок меню, задаваемый как json объект(формируемый динамически в зависимости от прав пользователя)
служебный блок различных оповещений
блок данных, в котором отрисовывается контент согласно текущего раздела меню
Так как Gitlab является постоянно развивающимся продуктом, его внешний вид довольно часто претерпевает изменения. При этом часть настроечных страниц для ce и ee версий очень сильно отличаются визуально. В связи с этим использовать внедрение в шаблоны haml становится невыгодно, т.к. нужно ориентироваться на конкретную версию gitlab. Поэтому наиболее удобной точкой внедрения является блок меню, который практически не меняется от версии к версии.
Внедряемся в основное меню
Меню Gitlab это очень интересная тема, учитывая то, что оно является полностью интерактивным и за его интерактивность отвечает большой кусок Javascript кода, хотя вся подготовка к магии происходит внутри ruby библиотеки. Проводя первые изыскания в части внедрения в Gitlab, я испробовал несколько вариантов, но множество из них приводило к 502 ошибке в puma и приходилось откатываться. Меню Gitlab, расположенное слева, в системе именуется как sidebar и имеет выделенный блок кода для своей работы. В новых версиях Gitlab появилось дополнительное меню справа, про него сегодня разговора не будет. Ниже по тексту статьи будет описана методика кастомизации меню в рамках создаваемого расширения.
Основная часть меню определяется библиотекой sidebar, расположенной по пути /opt/gitlab/embedded/service/gitlab-rails/lib/sidebars. По сути все части меню представляют собой конструкторы с набором параметров отвечающих за визуальное оформление пункта меню.
Gitlab имеет множество различных пунктов меню, но основным рабочим для большинства является "Your Work". Именно о нём дальше пойдёт речь.
С точки зрения кода ruby, меню "Your Work" представляет собой конструктор, описывающий топологию пунктов меню, находящийся в файле /opt/gitlab/embedded/service/gitlab-rails/lib/sidebars/your_work/panel.rb
# frozen_string_literal: true
module Sidebars
module YourWork
class Panel < ::Sidebars::Panel
override :configure_menus
def configure_menus
add_menus
end
override :aria_label
def aria_label
_('Your work')
end
override :super_sidebar_context_header
def super_sidebar_context_header
aria_label
end
private
def add_menus
return unless context.current_user
add_menu(Sidebars::YourWork::Menus::ProjectsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::GroupsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::OrganizationsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::IssuesMenu.new(context))
add_menu(Sidebars::YourWork::Menus::MergeRequestsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::TodosMenu.new(context))
add_menu(Sidebars::YourWork::Menus::MilestonesMenu.new(context))
add_menu(Sidebars::YourWork::Menus::SnippetsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::ActivityMenu.new(context))
end
end
end
end
Sidebars::YourWork::Panel.prepend_mod_with('Sidebars::YourWork::Panel')
где в свою очередь пункт меню Projects описывается как
# frozen_string_literal: true
module Sidebars
module YourWork
module Menus
class ProjectsMenu < ::Sidebars::Menu
override :link
def link
dashboard_projects_path
end
override :title
def title
_('Projects')
end
override :sprite_icon
def sprite_icon
'project'
end
override :render?
def render?
!!context.current_user
end
override :active_routes
def active_routes
{ controller: ['root', 'projects', 'dashboard/projects'] }
end
end
end
end
end
Как мы видим формат достаточно простой, но создание нового файла с описанием нашего меню и внедрение его при каждом обновлении может закончиться печально. Поэтому для внедрения в меню можно использовать короткую форму.
add_menu(::Sidebars::MenuItem.new(title: _('Helper Menu'), link: '/-/helper', sprite_icon: 'text-description', active_routes: {}, super_sidebar_parent: ::Sidebars::YourWork, item_id: :helper))
В итоге файл с меню "Your Work" будет выглядеть как
# frozen_string_literal: true
module Sidebars
module YourWork
class Panel < ::Sidebars::Panel
override :configure_menus
def configure_menus
add_menus
end
override :aria_label
def aria_label
_('Your work')
end
override :super_sidebar_context_header
def super_sidebar_context_header
aria_label
end
private
def add_menus
return unless context.current_user
add_menu(::Sidebars::MenuItem.new(title: _('Helper Menu'), link: '/-/helper', sprite_icon: 'text-description', active_routes: {}, super_sidebar_parent: ::Sidebars::YourWork, item_id: :helper))
add_menu(Sidebars::YourWork::Menus::ProjectsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::GroupsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::OrganizationsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::IssuesMenu.new(context))
add_menu(Sidebars::YourWork::Menus::MergeRequestsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::TodosMenu.new(context))
add_menu(Sidebars::YourWork::Menus::MilestonesMenu.new(context))
add_menu(Sidebars::YourWork::Menus::SnippetsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::ActivityMenu.new(context))
end
end
end
end
Sidebars::YourWork::Panel.prepend_mod_with('Sidebars::YourWork::Panel')
У нового пункта меню можно добавить персональную "иконку", взяв её название каталога https://gitlab-org.gitlab.io/gitlab-svgs/. После команды gitlab-ctl restart puma
мы увидим вот такой новый вид меню.
Как сделать изменения в меню постоянными ?
Возникает вопрос "Ну вот поправил я это меню, а при очередном обновлении опять править?". Для ответа на этот вопрос есть "магический" функционал триггеров, используя который мы всегда будем иметь наш пункт меню в этом файле.
При изучении структуры и принципа работы с триггерами я наткнулся на довольно простой пример на github https://github.com/Animalcule/triggered-edit-deb-package , рассматривая который становится понятен метод внедрения.
Для создания триггера нам необходимо описать точку, которая будет контролироваться при установке/обновлении пакета
interest /opt/gitlab/embedded/service/gitlab-rails/lib/sidebars/your_work/panel.rb
Далее необходимо описать скрипт, который будет вызываться при наступлении события с интересующим нас файлом.
#!/bin/sh
set -eu
if [ "$1" = "triggered" ]; then
if [ "$2" = "/opt/gitlab/embedded/service/gitlab-rails/lib/sidebars/your_work/panel.rb" ]; then
logger "Fix gitlab 'Your Work' menu"
line="return unless context.current_user"
addline="add_menu(::Sidebars::MenuItem.new(title: _('Helper Menu'), link: '/-/helper', sprite_icon: 'text-description', active_routes: {}, super_sidebar_parent: ::Sidebars::YourWork, item_id: :helper))"
sed -i -e "/$line$/a"'\\n'"\t$addline" "$2"
fi
fi
Из набора наших скриптов собирается deb пакет и устанавливается в систему. После этого при очередной замене файла panel.rb
на оригинальный/новый, наш триггер сработает и добавит в меню Gitlab нужный пункт. Модифицированный исходный код для генерации пакета и сам пакет можно скачать в репозитории https://github.com/aborche/gitlab-menu-changer/
Изначально предполагается, что пункт меню будет добавлен до вызова процедуры настройки Gitlab и запуска сервиса puma. Для тестирования работы скрипта можно просто вызвать исполняемый файл триггера с параметрами.
sh /var/lib/dpkg/info/gitlab-menu-changer.postinst triggered /opt/gitlab/embedded/service/gitlab-rails/lib/sidebars/your_work/panel.rb
После изменений необходимо выполнить команду gitlab-ctl restart puma
Что дальше ?
С меню вроде как разобрались, но возникает вопрос: "Что это нам даёт ? При нажатии на ссылку меню мы получаем 404/502 ошибку, ничего не работает!"
Для тех кто не совсем понял, что мы сделали в первой части, поясню. Мы создали новый элемент основного меню со ссылкой, находящейся в дереве основного сайта Gitlab, к которому применяются все правила работы в дереве сайта. Локальное хранилище, куки, csrf токены и прочая.
Здесь начинается вторая часть "магии". В самом начале статьи была ремарка по поводу Nginx, используемого для связки всех сервисов Gitlab при работе с пользователем. Мало кто задумывался о том, какую роль играет Nginx в экосистеме Gitlab, просто принимая его наличие как данность. В файле /etc/gitlab/gitlab.rb имеется пара закомментаренных строк
# nginx['custom_gitlab_server_config'] = "location ^~ /foo-namespace/bar-project/raw/ {\n deny all;\n}\n"
# nginx['custom_nginx_config'] = "include /etc/nginx/conf.d/example.conf;"
Первая описывает блокировку части контента, вторая позволяет добавить дополнительный конфиг для блока http. Это и является ключом к нашему расширению Gitlab. Для отображения нашего контента необходимо в блоке nginx['custom_gitlab_server_config']
описать внедрение нашего location
nginx['custom_gitlab_server_config'] = "include /etc/nginx/gitlab/helper.conf;"
и создать файл с location
location /-/helper {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-ARGS $args;
proxy_set_header X-Remote-Addr $remote_addr;
proxy_set_header X-Original-Host $host;
proxy_pass http://host.with.menu.app:8099;
}
location = /api/v4/graphql {
rewrite /api/v4/graphql /api/graphql last;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Ssl on;
proxy_read_timeout 900;
proxy_cache off;
proxy_buffering off;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_pass http://gitlab-workhorse;
}
После изменения /etc/gitlab/gitlab.rb необходимо выполнить gitlab-ctl reconfigure nginx
. При дальнейшей настройке файла с location можно использовать прямой перезапуск nginx командой gitlab-ctl restart nginx
. При этом проверить конфиг nginx можно командой /opt/gitlab/embedded/sbin/nginx -p /var/opt/gitlab/nginx/ -tT
В конфиге присутствует кусок для работы с graphql в ветке /api/v4, он необходим для использования в рамках функционала расширения, т.к. базовый функционал библиотеки go-gitlab(о нём ниже) "заточен" на префикс "/api/v4" и не позволяет выполнить запрос вне этого префикса.
Создаём наше расширение
Как ранее было написано, внедряясь в дерево сайта Gitlab мы получаем все привилегии и всё окружение для страниц Gitlab. Первые варианты расширения были построены в виде статических страниц на bootstrap, которые просто отрисовывали контент путём вызова api из ajax запросов. Но хотелось большего и было принято решение пронаследовать формат страниц Gitlab. Шаблонизатор Gitlab в любом случае отрисовывает страницы практически с нуля. Да есть небольшой кеш, но он работает на 3-4 обновления страницы. Поэтому вызов страницы Gitlab из стороннего приложения, с передачей необходимых кук и заголовков, ничем не будет отличаться от работы из браузера. В итоге был написан middleware сервер основной задачей которого было переформатирование страницы Gitlab в нужный формат. В качестве языка разработки был выбран Go 1.21, т.к. во первых мне понравился Gin framework, во вторых хотелось сравнить удобство Go в части обработки *ML структур по сравнению с python, java. К слову будет сказано, что на Go написан очень хороший модуль работы с api Gitlab https://github.com/xanzy/go-gitlab, который правда пришлось немного поправить для работы с куками.
Начнём с требований, что необходимо для нашего middleware сервиса и какие у нас ограничения ?
Middleware сервис должен обеспечивать следующий функционал:
Формирование запроса к базовой/начальной странице Gitlab для получения слепка
Отрабатывать ошибки аутентификации и редиректа для ввода аутентификационных данных
Уметь парсить полученный слепок и модифицировать его во внутренний шаблон
Обогащать полученный шаблон новым меню, а также формировать необходимый контент для визуализации
Отсюда возникают некоторые ограничения с которыми надо считаться:
Контекстные куки. Должны читаться, обрабатываться, обогащаться и возвращаться в текущую сессию пользователя всегда. Об этом чуть позже.
Обработка, парсинг и формирование шаблона может негативно отразиться на быстродействии. Это следует принимать как данность. Все должны понимать, что мы не работаем внутри realtime движка Gitlab, а занимаемся визуальной интеграцией в интерфейс.
Любая страница имеет блок настроек для Vue.js, поэтому внутренние шаблоны из которых будет строиться наше отображение контента должны учитывать этот блок настроек.
Отрисовка элементов на странице не может быть полностью аналогичной отрисовке Gitlab. Связано это с тем, что весь интерактив на странице это работа с динамическими dom объектами при помощи Vue.js скриптов и повторять это всё себе дороже. Об этом инфомация будет дальше
Использование Gitlab API возможно в полном объёме, за исключением некоторых методов, например "генерация deploy токена", где используется graphql + csrf. Об этом нужно помнить
Исходя из вышенаписанного начнём моделировать технологию взаимодействия middleware с Gitlab.
Сессионные куки
Пожалуй одна из важнейших частей middleware. В рамках сессионного взаимодействия Gitlab с пользователем используется различный набор кук и не всё из этого набора реально нужно для middleware. То что точно нам нужно:
known_sign_in - кука выставляемая при входе, на основании которой Gitlab "понимает" был ли ранее вход с указанной машины или нет
_gitlab_session - основная сессионная кука выставляемая при успешном входе в Gitlab. При отсутствии куки пользователь отправляется на страницу ввода логина/пароля
remember_user_token - токен формируемый при использовании checkbox "Remember me", участвующий в воссоздании сессионной куки "_gitlab_session" при её отсутствии
О чём необходимо помнить ?
Кука полученная middleware после передачи "remember_user_token" в Gitlab, должна в обязательном порядке быть сохранена в сессии middleware при работе с Gitlab API и после выставлена браузеру при отображении запрошенного контента
Истекшая кука "_gitlab_session" может быть автоматически заменена на новую при использовании "remember_user_token", что также потребует выполнения действий из п.1
Использование graphql с истёкшими куками на множестве запросов не включает режим ошибки авторизации. В связи с чем множество запросов просто возвращают пустой набор данных. Поэтому при работе с graphql нужна двойная обработка куки и последующая обработка csrf токена.
При отсутствии сессионной куки "_gitlab_session" Gitlab отправляет браузеру ответ "Status:302" и middleware должна уметь "ловить" этот момент.
Парсер страниц Gitlab
При выборе парсера для обработки страниц Gitlab используемых для шаблона middleware, следует учитывать, что на выходе парсера должен возвращаться видоизменённый шаблон, готовый к внедрению необходимого нам контента. В 99% случаев вы будете иметь дело с dom парсером, не учитывающим путь к элементам документа. Поэтому часть функционала нужно будет адаптировать
Что и как нужно парсить ?
Блок настроек. Находится в самом начале документа в блоке <script>, и единственным опознавательным знаком блока является его начало с "window.gon={};". Поэтому нужен фильтр по элементам <script> и контекстный поиск на наличие строки
Блок меню. Находится в теле страницы в элементе <aside> в аттрибуте "data-sidebar". При обработке блока меню требуется чёткое понимание того, что вы хотите получить, т.к. это очень тонкий и чувствительный элемент системы.
Блок контента. Находится в теле страницы в элементе <main>. Для правильной визуализации рекомендуется создать новую ноду <main>, скопировать в неё идентификатор и текущие атрибуты старого элемента, заполнить новый элемент переменной используемой при рендере шаблона и полностью удалить старый элемент. Все эти действия нужны для корректной работы css стилей Gitlab в новом блоке данных. Т.к. элемент <main> также подвержен изменениям со стороны vue.js
CSRF токен. На странице graphql-explorer токен находится в элементе с идентификатором "graphql-container" в аттрибуте "data-headers". На остальных страницах в блоке meta в виде
<meta name="csrf-param" content="authenticity_token" /> <meta name="csrf-token" content="RC5-m9R-db9Q" />
Данные из этого элемента требуется в обязательном порядке передавать в блок graphql api в заголовках. Не забываем по "_gitlab_session" куку, которая так же необходима при работе с graphql.
На этом основные требования к обработке исходной страницы заканчиваются, если хочется поменять что-то из других блоков вне элементов <main> и <aside>, это делается ровно таким же образом. Как пример блок навигации <nav>.
Блок меню и его структура
Блок меню имеет очень сложную структуру, но она вполне укладывается в конструкторы Go.
type GitlabSidebar struct {
AdminMode AdminMode `json:"admin_mode"`
AdminURL string `json:"admin_url"`
AvatarURL string `json:"avatar_url"`
CanSignOut bool `json:"can_sign_out"`
CanaryToggleCOMURL string `json:"canary_toggle_com_url"`
ContextSwitcherLinks []ContextSwitcherLink `json:"context_switcher_links"`
CreateNewMenuGroups []CreateNewMenuGroup `json:"create_new_menu_groups"`
CurrentContext CurrentContext `json:"current_context"`
CurrentContextHeader string `json:"current_context_header"`
CurrentMenuItems []CurrentMenuItem `json:"current_menu_items"`
DisplayWhatsNew bool `json:"display_whats_new"`
GitlabCOMAndCanary bool `json:"gitlab_com_and_canary"`
GitlabCOMButNotCanary bool `json:"gitlab_com_but_not_canary"`
GitlabVersion GitlabVersion `json:"gitlab_version"`
GitlabVersionCheck interface{} `json:"gitlab_version_check"`
GroupsPath string `json:"groups_path"`
HasLinkToProfile bool `json:"has_link_to_profile"`
IsAdmin bool `json:"is_admin"`
IsImpersonating bool `json:"is_impersonating"`
IsLoggedIn bool `json:"is_logged_in"`
IssuesDashboardPath string `json:"issues_dashboard_path"`
LinkToProfile string `json:"link_to_profile"`
LogoURL interface{} `json:"logo_url"`
MergeRequestMenu []MergeRequestMenu `json:"merge_request_menu"`
Name string `json:"name"`
PanelType string `json:"panel_type"`
PinnedItems []interface{} `json:"pinned_items"`
ProjectsPath string `json:"projects_path"`
Search Search `json:"search"`
Settings Settings `json:"settings"`
ShortcutLinks []ShortcutLink `json:"shortcut_links"`
ShowVersionCheck bool `json:"show_version_check"`
SignOutLink string `json:"sign_out_link"`
Status Status `json:"status"`
StopImpersonationPath string `json:"stop_impersonation_path"`
SupportPath string `json:"support_path"`
TodosDashboardPath string `json:"todos_dashboard_path"`
TrackVisitsPath string `json:"track_visits_path"`
UpdatePinsURL string `json:"update_pins_url"`
UserCounts UserCounts `json:"user_counts"`
Username string `json:"username"`
WhatsNewMostRecentReleaseItemsCount int64 `json:"whats_new_most_recent_release_items_count"`
WhatsNewVersionDigest string `json:"whats_new_version_digest"`
}
Большинство имеющихся в меню блоков данных при разработке расширения не нужны и их можно отключить. Путём операций Marshall/Unmarshall достаточно аккуратно изменить содержимое меню и заложить в него необходимый нам вид. Ниже пример того, как модифицируется существующее меню.
func TransformSidebar(c *gin.Context, cfg conf.Config, source *html.Node, fromFile bool) ([]byte, error) {
// Get sidebar source from Gitlab page
GitlabParsedSidebar, err := parsers.GetSideBarMenu(source)
if err != nil {
return nil, err
}
var SourceSideBar GitlabSidebar
err = json.Unmarshal([]byte(GitlabParsedSidebar), &SourceSideBar)
if err != nil {
return nil, err
}
// Remove from menu unused items
SourceSideBar.AdminMode.UserIsAdmin = false
SourceSideBar.IsAdmin = false
SourceSideBar.CanSignOut = false
SourceSideBar.Status.CanUpdate = false
SourceSideBar.HasLinkToProfile = false
SourceSideBar.Settings.HasSettings = false
SourceSideBar.ContextSwitcherLinks = []ContextSwitcherLink{}
SourceSideBar.ShortcutLinks = []ShortcutLink{}
SourceSideBar.MergeRequestMenu = []MergeRequestMenu{}
SourceSideBar.CreateNewMenuGroups = []CreateNewMenuGroup{}
SourceSideBar.DisplayWhatsNew = false
if fromFile {
SourceSideBar.CurrentMenuItems = BuildSidebarMenuFromFile(c, cfg)
} else {
SourceSideBar.CurrentMenuItems = BuildSidebarMenuFromStruct(c, cfg)
}
return json.Marshal(SourceSideBar)
}
Перейдём к структуре элементов меню. Они подчиняются общему конструктору вида
type CurrentMenuItem struct {
ActiveRoutes *ActiveRoutes `json:"active_routes,omitempty"`
Avatar interface{} `json:"avatar"`
EntityID interface{} `json:"entity_id"`
Icon string `json:"icon"`
ID string `json:"id"`
Link string `json:"link"`
LinkClasses interface{} `json:"link_classes"`
PillCount interface{} `json:"pill_count"`
Title string `json:"title"`
AvatarShape string `json:"avatar_shape,omitempty"`
IsActive bool `json:"is_active,omitempty"`
Items []CurrentMenuItemItem `json:"items,omitempty"`
Separated bool `json:"separated,omitempty"`
}
type ActiveRoutes struct {
Controller string `json:"controller"`
}
type CurrentMenuItemItem struct {
Avatar interface{} `json:"avatar"`
AvatarShape string `json:"avatar_shape,omitempty"`
EntityID interface{} `json:"entity_id"`
Icon interface{} `json:"icon"`
ID string `json:"id"`
IsActive bool `json:"is_active"`
Link string `json:"link"`
LinkClasses interface{} `json:"link_classes"`
PillCount *int `json:"pill_count"`
Title string `json:"title"`
}
По названиям элементов можно легко понять за что каждый элемент отвечает. Самые важные элементы:
id - уникальный идентификатор элемента в меню
icon - имя картинки из системного набора иконок и картинок
is_active - устанавливается в true, если пункт меню активен. При использовании вложенных меню родительский и подчинённый элемент должны иметь значение is_active: true
link - внешняя или внутренняя ссылка, указывающая куда нужно переключить пользователя при нажатии на пункт меню
pill_count - числовая метка, добавляемая после поля title, для отключения используется null
title - текст на кнопке меню
link_classes - дополнительные классы стиля добавляемые к пункту меню
Используя конструктор CurrentMenuItemItem, можно создать массив из нескольких подменю.
Пример меню для блока деплоя используемого у нас
[
{
"avatar": null,
"entity_id": null,
"icon": "go-back",
"id": "back",
"link": "/",
"link_classes": null,
"pill_count": null,
"title": "Back to Gitlab"
},
{
"avatar": null,
"entity_id": null,
"icon": "information-o",
"id": "helper",
"link": "/-/helper/",
"link_classes": null,
"pill_count": null,
"title": "Deploy Information"
},
{
"avatar": null,
"entity_id": null,
"icon": "rocket-launch",
"id": "deploy",
"link": "/-/helper/deploy/to-development",
"link_classes": "show",
"pill_count": null,
"title": "Deploy Area (Alpha)",
"is_active": false
},
{
"avatar": null,
"entity_id": null,
"icon": "requirements",
"id": "projects",
"link": "/-/helper/projects",
"link_classes": "show",
"pill_count": null,
"title": "Projects Audit (GraphQL)",
"is_active": false
},
{
"avatar": null,
"entity_id": null,
"icon": "users",
"id": "users",
"link": "/-/helper/users",
"link_classes": "",
"pill_count": null,
"title": "Users Audit (GraphQL)",
"is_active": false
}
]
Которое будет выглядеть у пользователя вот в таком виде.
Красиво, не правда ли ? :)
Основное окно контента
Меню мы настроили, теперь дело за основным окном с контентом.
Вариантов заполнения элементов основного окна несколько:
Предварительный рендер статической страницы на стороне сервера
Предварительный рендер шаблона на стороне сервера с использованием динамических наборов данных отрисовываемых правилами шаблона
Предварительный рендер шаблона с динамическим внешним набором данных
Предварительный рендер шаблона с получением данных из API Gitlab
Как ранее было написано, практически весь интерактивный интерфейс Gitlab построен на Vue.JS. К сожалению использовать в чистом виде весь набор функционала интерактивного интерфейса "в лоб" не получится. Но как показали исследования, css файлы Gitlab имеют базовые стили для всех типовых элементов контента и навигации. Поддерживается flex с группировками элементов, form-group, modal, dialog, стили для кнопок и многое другое. Для желающих изучить библиотеку Gitlab UI рекомендую пройти по ссылке https://gitlab-org.gitlab.io/gitlab-ui/. Красивыми интерфейсами конечно сходу похвастаться нельзя, но подобрать нужный набор стилей для элементов вполне возможно. Основная проблема заключается в том, что большинство стилей и цветов жёстко привязаны к цветовым схемам(светлая/тёмная). Поэтому построение необходимого интерфейса это достаточно кропотливая работа.
Переключение тем поддерживается автоматически
Для самостоятельных экспериментов я сформировал демонстрационный репозиторий https://github.com/aborche/gitlab-menu-extender-demo, в котором собран тестовый функционал для ознакомления. Качество кода для профессиональных разработчиков может быть "ниочень", но тут смысл не в красоте кода, а в демонстрации возможностей по добавлению функционала и изменению интерфейса под свои нужды.
У нас в организации данный функционал уже активно тестируется и используется для внутренних проектов. Используя демонстрационный проект довольно просто можно создать новые пункты меню и обогатить их создав новый шаблон страницы для отрисовки.
Надеюсь данный материал был вам полезен, удачи в моддинге Gitlab :)
TyVik
Не специалист в ruby и gitlab, но часть недостающего функционала реализовал с помощью браузерного tampermonkey. Скрипт, правда, пришлось всем участникам поставить вручную, но на команду из 10 человек это было ок.
Как пример - двойной аппрув, доступный только в платной версии, сделал на эмодзи и кастомной кнопке.