Как стать автором
Обновить

Расширяем меню и функционал Gitlab

Уровень сложностиСредний
Время на прочтение17 мин
Количество просмотров2.2K

Во время быстрого развития ИТ среды, многие её активные участники пользуются готовыми решениями обеспечивающими их определённым функционалом, который хотелось бы расширить. Но расширение продукта зачастую является либо платным, либо чрезвычайно затратным действием, требующим постоянного контроля доступной кодовой базы, адаптацией и корректировкой своей части для совместимости. Приходится создавать продукты размещаемые "рядом" и различными способами производить интеграцию. Попробуем изменить это с помощью минимальных затрат и расширить функционал 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". Именно о нём дальше пойдёт речь.

Базовый вид меню "Your Work"
Базовый вид меню "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 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" при её отсутствии

О чём необходимо помнить ?

  1. Кука полученная middleware после передачи "remember_user_token" в Gitlab, должна в обязательном порядке быть сохранена в сессии middleware при работе с Gitlab API и после выставлена браузеру при отображении запрошенного контента

  2. Истекшая кука "_gitlab_session" может быть автоматически заменена на новую при использовании "remember_user_token", что также потребует выполнения действий из п.1

  3. Использование graphql с истёкшими куками на множестве запросов не включает режим ошибки авторизации. В связи с чем множество запросов просто возвращают пустой набор данных. Поэтому при работе с graphql нужна двойная обработка куки и последующая обработка csrf токена.

  4. При отсутствии сессионной куки "_gitlab_session" Gitlab отправляет браузеру ответ "Status:302" и middleware должна уметь "ловить" этот момент.

Парсер страниц Gitlab

При выборе парсера для обработки страниц Gitlab используемых для шаблона middleware, следует учитывать, что на выходе парсера должен возвращаться видоизменённый шаблон, готовый к внедрению необходимого нам контента. В 99% случаев вы будете иметь дело с dom парсером, не учитывающим путь к элементам документа. Поэтому часть функционала нужно будет адаптировать

Что и как нужно парсить ?

  1. Блок настроек. Находится в самом начале документа в блоке <script>, и единственным опознавательным знаком блока является его начало с "window.gon={};". Поэтому нужен фильтр по элементам <script> и контекстный поиск на наличие строки

  2. Блок меню. Находится в теле страницы в элементе <aside> в аттрибуте "data-sidebar". При обработке блока меню требуется чёткое понимание того, что вы хотите получить, т.к. это очень тонкий и чувствительный элемент системы.

  3. Блок контента. Находится в теле страницы в элементе <main>. Для правильной визуализации рекомендуется создать новую ноду <main>, скопировать в неё идентификатор и текущие атрибуты старого элемента, заполнить новый элемент переменной используемой при рендере шаблона и полностью удалить старый элемент. Все эти действия нужны для корректной работы css стилей Gitlab в новом блоке данных. Т.к. элемент <main> также подвержен изменениям со стороны vue.js

  4. 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
  }
]

Которое будет выглядеть у пользователя вот в таком виде.

Красиво, не правда ли ? :)

Основное окно контента

Меню мы настроили, теперь дело за основным окном с контентом.

Вариантов заполнения элементов основного окна несколько:

  1. Предварительный рендер статической страницы на стороне сервера

  2. Предварительный рендер шаблона на стороне сервера с использованием динамических наборов данных отрисовываемых правилами шаблона

  3. Предварительный рендер шаблона с динамическим внешним набором данных

  4. Предварительный рендер шаблона с получением данных из 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 :)

(C) Aborche 2024
(C) Aborche 2024

Теги:
Хабы:
+15
Комментарии1

Публикации

Истории

Работа

Ближайшие события

Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область