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

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

Время на прочтение8 мин
Количество просмотров1.1K

Привет, читатели, это вторая часть обучающих постов о написании 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)

Теги:
Хабы:
Рейтинг0
Комментарии0

Публикации