Slack Ruby App. Часть 2. Добавление чартов, или как делать рендер фронта на сервере

от автора

Привет, читатели, это вторая часть обучающих постов о написании Slack App с использованием чистого Ruby.

Если вы не знакомы со списком частей, то вот он (со ссылками):

  1. Написание приложения локально через Sinatra и ngrok.

  2. Добавление чартов, или как делать рендер фронта на сервере (Мы здесь). 

  3. Тусовка приложения с таким гостем, как Heroku.

Во время разработки своего приложения в качестве проекта внутри компании, поступил запрос на постройку графика по некоторой выборке данных, чтобы по команде Slash Command происходила постройка графика и его публикация в чат.

Первым делом я обратился к уже существующим решениям, графики через API Google, готовые гемы для Ruby. Основной минус в том, что не было возможности убрать или добавить легенду в том формате, который мне нужен, сложно кастомизировать внешний вид этих графиков и, к примеру, нет возможностей строить график по значению timestamp, а выводить уже значения в формате DateTime.

когда я говорю про сложность действия, то имею в виду в сравнении с альтернативным моим решением, где это делается проще и быстрее

Имея на вооружении тот метод, который освещает эта статья, любой сможет строить какую угодно страницу полностью на сервере, получать фото этой страницы и использовать её в коде. В будущем можно приспособить этот подход для, например, для предпоказа тем на своем движке, генерации каких-то изображений с подвязкой к внешнему API и заключении всего в html документ. В целом, применений реально много, собственно поэтому и решил поделится solution’ом.

Поэтому предлагаю вам скорее ознакомится с материалом, мы научимся сначала делать график с использованием своего js скрипта, а потом поймём как его рендерить в коде. Представляю содержание:

  1. Настройка модального окна через Slash Command для вызова построения графика

  2. Пишем html/js/css шаблон для графика

  3. Рендерим и получаем фото

  4. Вывод результата в модальное окно и отправка в чат

  5. Результаты

  6. Файл command.rb

  7. Ресурсы

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

Флоучарт для этого таска я вижу таким:

Изображение 1. Лучший флоучарт
Изображение 1. Лучший флоучарт


Настройка модального окна через Slash Command для вызова построения графика

Я вижу такую картину: сначала происходит запрос на Slash Command, мы его обрабатываем и в качестве ответа возвращаем view, в котором внутри уже находится график. Если пользователь хочет этот график пошарить в чат, то нажимает соответствующую кнопку, если нет — просто закрывает окно и уходит по своим делам.

Первым делом добавляем ендпоинт на панели управления приложением для команды : `/graph`.

Затем пишем простенький шаблон вывода, чтобы проверить, что всё норм работает, обработка вызова команды выглядит так:

  post '/graph' do     get_input     Database.init     access_token = Database.find_access_token input['team_id']     client = create_slack_client access_token      triger_id = input['trigger_id']      template = File.read './Components/View/graph.erb'     view = ERB.new(template).result(binding)      client.views_open(         trigger_id: triger_id,         view: view     )      status 200   end

Сам файл шаблона graph.erb как-то так :

{   "type": "modal",   "title": {     "type": "plain_text",     "text": "Граф"   },   "blocks": [     {       "type": "section",       "text": {         "type": "mrkdwn",         "text": "Тут будет график "       }     }   ] }

Собственно запускаем сервер, пишем в чате (в любом) `/graph` и видим такой результат:

Изображение 2. Превью модального окна с графом
Изображение 2. Превью модального окна с графом

Если у вас такой же результат, значит всё делаете правильно! Переходим к следующему этапу.


Пишем html/js/css шаблон для графика

Какая задумка дальше, опускаемся на уровень ниже. Пользователь имеет некоторые данные, по которым он хочет построить свой кастомный график, нужно решить:

В этом разделе я планирую рассмотреть первый и второй пункт.

Создадим папку в директории Components — Graph. В папке Graph нам нужно будет 3 файла : style.css, index.html и main.js (Изображение 3)

Изображение 3. Необходимые файлы для построение графика
Изображение 3. Необходимые файлы для построение графика

Данные проще всего сохранить в формате json, в файл. В js скрипте будем считать, что на момент запуска файл существует, так как мы сами и будем гарантировать существования данных через код. Значит — нужно написать функцию по получению данных в js скрипте:

function readTextFile(file, callback) {     var rawFile = new XMLHttpRequest();     rawFile.overrideMimeType("application/json");     rawFile.open("GET", file, false);     rawFile.onreadystatechange = function () {         if (rawFile.readyState === 4 && rawFile.status == "200") {             callback(rawFile.responseText);         }     }     rawFile.send(null); }

будем использовать функцию чтения из файла таким образом:

readTextFile('data.json', function (text) {         dataJson = (JSON.parse(text))   //Тут работа с данными которые записаны в dataJson переменную });

Следующий шаг — написать функцию, которая бы сохраняла данные в промежуточный файл — data.json. Данные будут в виде:

{    "2021":   {     "Jun":8207,     "Jul":12455,     "Aug":10086   },   "2022":   {     "Jul":1234,     "Oct":5678,     "Jan":9123   } }

Первый уровень обозначает год, второй уровень — месяц и значение месяца — timestamp какой-то операции. Я хочу увидеть статистику по месяцам, по данной операции. Если вы не против, я запишу эти данные как константу и не буду придумывать кейсы, где эти данные взять и что они означают 🙂

Запишем данные в файл, в директорию — Graph. Коротко это можно сделать так:

    DATA_TO_JSON = {"2021": {"Jun": 8207, "Jul": 12455, "Aug": 10086}, "2022": {"Jul": 1234, "Oct": 5678, "Jan": 9123}}        File.open('./Components/Graph/data.json', 'w') do |file|       file.write DATA_TO_JSON.to_json end

ОБЯЗ! Данные, не важно в каком они формате записаны в Ruby — переводим в json для того что бы js мог их считать.

Теперь опишу сам скрипт и что там к чему

В индекс файле лежит div, который имеет id = chart. В скрипте я строю Dom элемент, он и есть график, и просто засовываю его в этот div с чартом. В будущем можна в любом месте, на любом сайте поставить такой div=chart и туда подгрузится график (файл style.css и main.js тоже должны присутствовать, так как они отвечают за работоспособность).

Сначала создается элемент div, в нём таблица, добавляются по одному столбцы с динамической высотой, равной timestamp, потом в зависимости от того, в каком диапазоне находится время , столбцу присваивается цвет, потом легенда сверху и ленеда снизу. Перед тем, как ставить легенду сверху — превращаем её во время формата HH:MM:SS. Значение в нижней легенде равно ключу второго уровня + ключ первого уровня.

Собственно и всё. Получаем данные из файла, в цикле создаём по ним график, добавляем график на html.


Рендерим и получаем фото

Собственно ключевая вещь в этом туториале. Когда я захотел сделать такой чарт, то не задумался о том, как же собственно html будет обрабатыватся сервером. Ну, типо, есть теги, это всё круто отображается в браузере как надо.

И тут то я прозрел, что html — язык разметки браузера, а браузера то на сервере нет 🙂

Много времени лично я потратил, чтобы понять, что делать с этим вот всем и нашёл решение в лице Chrom Headless, что буквально значит хром, но без head (head-морда, фронтенд, оболочка). Работает всё очень классно и просто, движок хрома может отпарсить и рендерить любую страницу, js встроенный есть и все дела. Вообще всё то же самое, что и в вашем браузере, но общаетесь с ним на кликами, а командами.

Нам нужен новый гем — `gem «ferrum»`, добавьте его к себе в Gemfile и запустите bundle update.

Далее в обработчике команды пропишем:

    browser = Ferrum::Browser.new     browser.go_to("https://#{request.host}/render_graph")     browser.screenshot(path: "Components/Graph/result.png")

Собственно всё просто и понятно, вот только когда мы попытаемся попасть на адресс

"https://#{request.host}/render_graph"

то явно не попадём на index.html, чтобы это исправить — добавим в файл command.rb обработку get запроса для страницы render_graph, и тут же для остальных наших файлов, так как обращение к ним будет идти по такому же «неточному» пути.

  get '/render_graph' do     send_file 'Components/Graph/index.html'   end    get '/main.js' do     send_file 'Components/Graph/main.js'   end    get '/style.css' do     send_file 'Components/Graph/style.css'   end    get '/data.json' do     send_file 'Components/Graph/data.json'   end

Всё готово к запуску, добавим require к классу, в котором будем вызывать библиотеку Headless браузера :

require 'sinatra/base' require 'slack-ruby-client'  class API < Sinatra::Base   require 'ferrum'

Перезапускаем сервер и пишем /graph, затем смотрим в папку Graph и наблюдаем файл result.png который выглядит вот так:

Изображение 4. Ну просто юхуууу
Изображение 4. Ну просто юхуууу


Вывод результата в модальное окно и отправка в чат

Теперь дело за малым:

  • Строим модальный блок

  • Отправляем этот блок пользователю

Строим модальный блок

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

{  "type": "image",  "image_url": "http://<%= host %>/graph_image.png",  "alt_text": "Сер Граф" }

Редактируем весь файл graph.erb к такому виду :

{   "type": "modal",   "title": {     "type": "plain_text",     "text": "Граф"   },   "blocks": [     {       "type": "image",       "image_url": "http://<%= host %>/graph_image.png",       "alt_text": "Сер Граф"     }   ] }

Тут, по аналогии с примером index.html страницы, необходимо по запросу {host}/graph_image.png вернуть result.png, настроить этот запрос-ответ с помощью такого кода :

  get '/graph_image.png' do     send_file 'Components/Graph/result.png'   end 

Так же, для заполнения <%= host %> , необходимо, перед вызовом шаблона, добавить инициализацию переменной host

host = request.host

Отправляем этот блок пользователю

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

Могу лишь добавить, что после отправки результата на Slack API, нам не нужен ни результат, ни data.json. Удаляем их:

    File.delete "./Components/Graph/result.png"     File.delete "./Components/Graph/data.json"


Результаты

Запустим в чате команду /graph и понаблюдаем что будет в логе и что будет видно юзеру :

Изображение 5. График в модальном окне
Изображение 5. График в модальном окне
Изображение 6. Лог сервера
Изображение 6. Лог сервера

Собственно результат ровно тот, что мы ожидали. Ладно, не ровно такой, я говорил о кнопке отправки в чат, но есть ли смысл мне это реализовывать, если в этом и в прошлом уроке я подробно описал как постить сообщения, как обрабатывать модальные команды и всякие такие штуки. Даже блоки уже готовы, нужно лишь поменять в graph.erb хидер с модального окна на нужный вам.

В итоге мы научились :

  • Рендерить html/css/js c помощью Headless браузера на сервере.

  • Строить график с необходимыми нам параметрами.

  • Выводить изображения в модальные окна.

  • Отправлять файлы по запросу на сервер.

  • Сохранять данные в файл.


Файл command.rb

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

require 'sinatra/base' require 'slack-ruby-client'  class API < Sinatra::Base   require 'ferrum'    attr_accessor :access_token   attr_accessor :input    def get_input     self.input = Rack::Utils.parse_nested_query(request.body.read)     puts 'Получена входная строка'   end    get '/test' do     status 302     redirect "https://slack.com/oauth/v2/authorize?&client_id=#{SLACK_CONFIG[:slack_client_id]}&scope=app_mentions:read,channels:history,channels:read,chat:write,chat:write.customize,commands,files:write,groups:history,groups:read,im:history,im:read,mpim:read,users.profile:read,users:read&user_scope=channels:history,channels:read&redirect_uri=#{SLACK_CONFIG[:redirect_uri]}"   end    post '/who_am_i' do     get_input     Helper.displayUserInfo input['team_id'], input['user_id'], input['channel_id']     status 200   end    post '/menu' do     get_input     Database.init     access_token = Database.find_access_token input['team_id']     client = create_slack_client access_token      triger_id = input['trigger_id']     metadata = input['channel_id']      template = File.read './Components/View/menu.erb'     view = ERB.new(template).result(binding)      client.views_open(         trigger_id: triger_id,         view: view     )      status 200   end    get '/render_graph' do     send_file 'Components/Graph/index.html'   end    get '/main.js' do     send_file 'Components/Graph/main.js'   end    get '/style.css' do     send_file 'Components/Graph/style.css'   end    get '/data.json' do     send_file 'Components/Graph/data.json'   end    get '/graph_image.png' do     send_file 'Components/Graph/result.png'   end    post '/graph' do     get_input      DATA_TO_JSON = {"2021": {"Jun": 8207, "Jul": 12455, "Aug": 10086}, "2022": {"Jul": 1234, "Oct": 5678, "Jan": 9123}}     File.open('./Components/Graph/data.json', 'w') do |file|       file.write DATA_TO_JSON.to_json     end      browser = Ferrum::Browser.new     browser.go_to("https://#{request.host}/render_graph")     browser.screenshot(path: "Components/Graph/result.png")      Database.init     access_token = Database.find_access_token input['team_id']     client = create_slack_client access_token      triger_id = input['trigger_id']      host = request.host     template = File.read './Components/View/graph.erb'     view = ERB.new(template).result(binding)      client.views_open(         trigger_id: triger_id,         view: view     )      File.delete "./Components/Graph/result.png"     File.delete "./Components/Graph/data.json"      status 200   end end 
  • Часть кода осталась с прошлого урока 🙂


Ресурсы

Ссылка на Github репозиторий с реализацией этой части + прошлой части

(ветка my_graph)


ссылка на оригинал статьи https://habr.com/ru/articles/578798/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *